Java 类的反射

一、什么是反射?

Java反射说的是在运行状态中,对于任何一个类,我们都能够知道这个类有哪些方法和属性。对于任何一个对象,我们都能够对它的方法和属性进行调用。我们把这种动态获取对象信息和调用对象方法的功能称之为反射机制。

二、反射的三种方式

这里需要跟大家说一下,所谓反射其实是获取类的字节码文件,也就是.class文件,那么我们就可以通过Class这个对象进行获取。

1、第一种方式

getClass

public final Class<?> getClass()

Returns the runtime class of this Object. The returned Class object is the object that is locked by static synchronized methods of the represented class.

返回类的运行实例

这个方法其实是Object的一个方法,Class继承了Object,所以我们可以直接使用。

package cn.leokim;

public class Reflact {
    public static void main(String[] args) {
        //创建一个对象
        Test t = new Test();

        //获取该对象的Class对象
        Class c = t.getClass();

        //获取类名称
        System.out.println(c.getName());
    }
}

2.第二种方式

package cn.leokim;

public class Reflact {
    public static void main(String[] args) {
        Class c = Test.class

        //获取类名称
        System.out.println(c.getName());
    }
}

3.第三种方式

package cn.leokim;

public class Reflact {
    public static void main(String[] args) {
        //这里需要注意,通过类的全路径名获取Class对象会抛出一个异常,如果根据类路径找不到这个类那么就会抛出这个异常。
        try {
            Class c = Class.forName("cn.leokim.Test");

            //获取类名称
            System.out.println(c.getName());
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

那么这3中方式我们一般选用哪种方式呢?第一种已经创建了对象,那么这个时候就不需要去进行反射了,显得有点多此一举。第二种需要导入类的包,依赖性太强。所以我们一般选中第三种方式。

三、通过反射获取类的构造方法、方法以及属性

1、获取构造方法

package cn.leokim;

import java.lang.reflect.Constructor;

public class Reflact {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
        //加载Class对象
        Class c = Class.forName("cn.leokim.Test");

        System.out.println("======================= 获取所有公用的构造方法 =======================");
        //获取所有公用的构造方法
        Constructor[] constructors = c.getConstructors();

        for (Constructor constructor : constructors){
            System.out.println(constructor);
        }

        System.out.println("======================= 获取所有公用的构造方法 =======================");
        //获取所有构造方法
        Constructor[] declareConsturctors = c.getDeclaredConstructors();

        for (Constructor declareConsturctor : declareConsturctors){
            System.out.println(declareConsturctor);
        }

        System.out.println("======================= 获取所有 公有 & 无参 的构造方法 =======================");
        Constructor constructor = c.getConstructor(null);
        System.out.println(constructor);

        System.out.println("======================= 获取所有 公有 & 有参 的构造方法 =======================");
        Constructor constructor1 = c.getConstructor(String.class);
        System.out.println(constructor1);

        System.out.println("======================= 获取所有 私有 & 有参 的构造方法 =======================");
        Constructor declareConsturctors1 = c.getDeclaredConstructor(String.class, Integer.class);
        System.out.println(declareConsturctors1);
    }
}

结果:

image.png

2、获取类属性

Test.java

package cn.leokim;

public class Test {


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String name;

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    private String sex;

    public String public_field2;
    private String private_field3;


    public Test() {
        System.out.println("no args constructor");
    }

    public Test(String name){
        System.out.println("Test: "+ name);
    }

    private Test(String name, Integer age){
        System.out.println("Name: "+ name + "Age "+ age );
    }

    public void t(){
        System.out.println("print t function.");
    }

}

Reflact.java

package cn.leokim;

import sun.jvm.hotspot.oops.ObjectHeap;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class Reflact {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //加载Class对象
        Class c = Class.forName("cn.leokim.Test");

//        System.out.println("======================= 获取所有公用的构造方法 =======================");
//        //获取所有公用的构造方法
//        Constructor[] constructors = c.getConstructors();
//
//        for (Constructor constructor : constructors){
//            System.out.println(constructor);
//        }
//
//        System.out.println("======================= 获取所有公用的构造方法 =======================");
//        //获取所有构造方法
//        Constructor[] declareConsturctors = c.getDeclaredConstructors();
//
//        for (Constructor declareConsturctor : declareConsturctors){
//            System.out.println(declareConsturctor);
//        }
//
//        System.out.println("======================= 获取所有 公有 & 无参 的构造方法 =======================");
//        Constructor constructor = c.getConstructor(null);
//        System.out.println(constructor);
//
//        System.out.println("======================= 获取所有 公有 & 有参 的构造方法 =======================");
//        Constructor constructor1 = c.getConstructor(String.class);
//        System.out.println(constructor1);
//
//        System.out.println("======================= 获取所有 私有 & 有参 的构造方法 =======================");
//        Constructor declareConsturctors1 = c.getDeclaredConstructor(String.class, Integer.class);
//        System.out.println(declareConsturctors1);


        System.out.println("========= 获取所有的公共字段 ==========");
        Field[] fields = c.getFields();

        for(Field field : fields){
            System.out.println(field);
        }

        System.out.println("========= 获取所有的字段(public & private) ==========");
        Field[] declaredFields = c.getDeclaredFields();

        for(Field field : declaredFields){
            System.out.println(field);
        }

        System.out.println("========= 获取公有字段并使用 ==========");
        Field field = c.getField("name");
        Object obj = c.getConstructor().newInstance();

        //为属性设置值
        field.set(obj, "LeoKim");
        Test test = (Test) obj;
        System.out.println("Name is: " + test.getName());

        System.out.println("========= 获取私有字段并使用 ==========");
        Field field1 = c.getDeclaredField("sex");
        Object obj1 = c.getConstructor().newInstance();

        //暴力反射
        field1.setAccessible(true);
        field1.set(obj1, "男");
        Test test1 = (Test) obj1;
        System.out.println(test1.getSex());
    }
}[object Object]

这里需要注意,在获取私有属性的时候如果没有进行暴力反射,那么会抛出下面这个异常。

3.获取类中的方法

先定义几个方法

public void method1(String str){
    System.out.println("public method.");
}

private void method2(){
    System.out.println("private method.");
}

String method3(String name, String sex){
    System.out.println("default method: " + name + " & " + "sex");
    return name + sex;
}

protected  void method4(){
    System.out.println("protected method.");
}

正题:

package cn.leokim;

import sun.jvm.hotspot.oops.ObjectHeap;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Reflact {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InvocationTargetException, InstantiationException, InterruptedException {
        //加载Class对象
        Class c = Class.forName("cn.leokim.Test");

        System.out.println("================ 获取所有的public方法 ================");
        Method[] methods = c.getMethods();

        for (Method method : methods){
            System.out.println(method);
        }

        Thread.sleep(1000);

        System.out.println("================ 获取所有的方法 ================");
        Method[] declaredMethods = c.getDeclaredMethods();

        for (Method method : declaredMethods){
            System.out.println(method);
        }

        Thread.sleep(1000);
        
        System.out.println("================ 获取特定方法带参并使用 ================");
        Method method1 = c.getMethod("method1", String.class);
        Object obj1 = c.getConstructor().newInstance();
        Test test1 = (Test) obj1;
        test1.method1("test str");
        Thread.sleep(1000);

        System.out.println("================ 获取特定方法多个参数使用 ================");
        Method method3 = c.getDeclaredMethod("method3", String.class, String.class);
        Object obj = c.getConstructor().newInstance();
        //给方法传值
        Object invoke = method3.invoke(obj, "LeoKim", "男");

        System.out.println(invoke);
    }
}

image.png

四、反射执行main方法

public static void main(String[] args) {
    for (String arg : args) {
        System.out.println(arg);
    }
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Method method = Class.forName("cn.leokim.Test").getMethod("main", String[].class);
    method.invoke(null, (Object)new String[]{"111", "222", "333"});
}

Java基于接口的动态代理

所谓代理,就是不改变原有类代码的基础上,对其方法进行增强。这里以演员举例对其进行说明。演员有出演角色的行为,他们能演戏,但他们都会与经纪公司签约,而剧组找人都是找经纪公司,向公司提供一个标准。这个过程中,演员就是原生类,经纪公司就好比是在做代理这件事。经纪公司给他们公司的演员对外宣称,低于10000的戏不演,这就是在对演员的动作进行增强。代码如下:

接口:

public interface IActor {
	public void perform(int money,String name);
	public void dangerPerform(int money);
}

实现类:

package com.dimples.service.impl;
 
import com.dimples.service.IActor;
 
public class MyActor implements IActor {
	@Override
	public void perform(int money,String name) {
		System.out.println("拿到" + money + "钱,执行一般表演");
	}
 
	@Override
	public void dangerPerform(int money) {
		System.out.println("拿到" + money + "钱,执行特殊表演");
	}
}

测试类:

package com.dimples.test;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
import com.dimples.service.IActor;
import com.dimples.service.impl.MyActor;
 
public class Test2 {
 
	public static void main(String[] args) {
		//这里注意不是final类型在下面将不能被内部类引用
		final MyActor actor = new MyActor();
                //这就是在创建代理对象,第一个参数类加载器;第二个参数被代理对象实现的接口,好告知代理对象它需要具备哪些行为;第三个参数就是我们进行增强的代码
		IActor proxyActor = (IActor) Proxy.newProxyInstance(actor.getClass().getClassLoader(), actor.getClass().getInterfaces(), new InvocationHandler() {
			
			@Override
               //proxy代表代理对象,method表示执行的方法对象,args表示参数数组。被代理对象每个方法执行时都会过这个invoke方法
			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
				int money = (int) args[0];
				if("perform".equals(method.getName())){
					if(money>10000)
						method.invoke(actor, args);
				}else if("dangerPerform".equals(method.getName())){
					if(money>50000)
						method.invoke(actor, args);
				}
				return null;
			}
		});
		
		proxyActor.perform(10001, "hahaha");
		proxyActor.dangerPerform(50001);
	}
	
 
}

注意,基于接口的代理方式,被代理对象至少要实现一个接口!

JdbcTemplate基本使用

JDBC已经能够满足大部分用户最基本的需求,但是在使用JDBC时,必须自己来管理数据库资源如:获取PreparedStatement,设置SQL语句参数,关闭连接等步骤。

JdbcTemplate是Spring对JDBC的封装,目的是使JDBC更加易于使用。JdbcTemplate是Spring的一部分。JdbcTemplate处理了资源的建立和释放。他帮助我们避免一些常见的错误,比如忘了总要关闭连接。他运行核心的JDBC工作流,如Statement的建立和执行,而我们只需要提供SQL语句和提取结果。

Spring源码地址:https://github.com/spring-projects/spring-framework

在JdbcTemplate中执行SQL语句的方法大致分为3类:

execute:可以执行所有SQL语句,一般用于执行DDL语句。

update:用于执行INSERT、UPDATE、DELETE等DML语句。

queryXxx:用于DQL数据查询语句。

JdbcTemplate配置连接池

org.springframework.jdbc.core.JdbcTemplate类方便执行SQL语句

public JdbcTemplate(DataSource dataSource)

创建JdbcTemplate对象,方便执行SQL语句

public void execute(final String sql)

execute可以执行所有SQL语句,因为没有返回值,一般用于执行DDL语句。

JdbcTemplate使用步骤

准备DruidDataSource连接池

导入依赖的jar包

spring-beans-4.1.2.RELEASE.jar

spring-core-4.1.2.RELEASE.jar

spring-jdbc-4.1.2.RELEASE.jar

spring-tx-4.1.2.RELEASE.jar

com.springsource.org.apache.commons.logging-1.1.1.jar

创建JdbcTemplate对象,传入Druid连接池

调用execute、update、queryXxx等方法

案例代码

public class Demo04 {
public static void main(String[] args) {
    // 创建表的SQL语句
    String sql = "CREATE TABLE product("
    + "pid INT PRIMARY KEY AUTO_INCREMENT,"
    + "pname VARCHAR(20),"
    + "price DOUBLE"
    + ");";
    JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
    jdbcTemplate.execute(sql);
    }
}

JdbcTemplate实现增删改

API介绍

org.springframework.jdbc.core.JdbcTemplate类方便执行SQL语句

public int update(final String sql)

用于执行`INSERT`、`UPDATE`、`DELETE`等DML语句。

使用步骤

1.创建JdbcTemplate对象

2.编写SQL语句

3.使用JdbcTemplate对象的update方法进行增删改

案例代码

public class Demo05 {
    public static void main(String[] args) throws Exception {
    //test01();
    //test02();
    //test03();
    }
    // JDBCTemplate添加数据
    public static void test01() throws Exception {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
        String sql = "INSERT INTO product VALUES (NULL, ?, ?);";
        jdbcTemplate.update(sql, "iPhone3GS", 3333);
        jdbcTemplate.update(sql, "iPhone4", 5000);
        jdbcTemplate.update(sql, "iPhone4S", 5001);
        jdbcTemplate.update(sql, "iPhone5", 5555);
        jdbcTemplate.update(sql, "iPhone5C", 3888);
        jdbcTemplate.update(sql, "iPhone5S", 5666);
        jdbcTemplate.update(sql, "iPhone6", 6666);
        jdbcTemplate.update(sql, "iPhone6S", 7000);
        jdbcTemplate.update(sql, "iPhone6SP", 7777);
        jdbcTemplate.update(sql, "iPhoneX", 8888);
    }
    // JDBCTemplate修改数据
    public static void test02() throws Exception {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
        String sql = "UPDATE product SET pname=?, price=? WHERE pid=?;";
        int i = jdbcTemplate.update(sql, "XVIII", 18888, 10);
        System.out.println("影响的行数: " + i);
    }
    // JDBCTemplate删除数据
    public static void test03() throws Exception {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
        String sql = "DELETE FROM product WHERE pid=?;";
        int i = jdbcTemplate.update(sql, 7);
        System.out.println("影响的行数: " + i);
    }
}

JdbcTemplate查询-queryForInt返回一个int整数

org.springframework.jdbc.core.JdbcTemplate类方便执行SQL语句

API介绍

public int queryForInt(String sql)

执行查询语句,返回一个int类型的值。

使用步骤

创建JdbcTemplate对象

编写查询的SQL语句

使用JdbcTemplate对象的queryForInt方法

输出结果

案例代码

// queryForInt返回一个整数
public static void test01() throws Exception {
   // String sql = "SELECT COUNT(*) FROM product;";
   String sql = "SELECT pid FROM product WHERE price=18888;";
   JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
   int forInt = jdbcTemplate.queryForInt(sql);
   System.out.println(forInt);
}

JdbcTemplate查询-queryForLong返回一个long整数

讲解

org.springframework.jdbc.core.JdbcTemplate类方便执行SQL语句

API介绍

public long queryForLong(String sql)

执行查询语句,返回一个long类型的数据。

使用步骤

创建JdbcTemplate对象

编写查询的SQL语句

使用JdbcTemplate对象的queryForLong方法

输出结果

案例代码

// queryForLong  返回一个long类型整数
public static void test02() throws Exception {
   String sql = "SELECT COUNT(*) FROM product;";
   // String sql = "SELECT pid FROM product WHERE price=18888;";
   JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
   long forLong = jdbcTemplate.queryForLong(sql);
   System.out.println(forLong);
}

JdbcTemplate查询-queryForObject返回String

讲解

org.springframework.jdbc.core.JdbcTemplate类方便执行SQL语句

API介绍

public <T> T queryForObject(String sql, Class<T> requiredType)

执行查询语句,返回一个指定类型的数据。

使用步骤

创建JdbcTemplate对象

编写查询的SQL语句

使用JdbcTemplate对象的queryForObject方法,并传入需要返回的数据的类型

输出结果

案例代码

public static void test03() throws Exception {
   String sql = "SELECT pname FROM product WHERE price=7777;";
   JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
   String str = jdbcTemplate.queryForObject(sql, String.class);
   System.out.println(str);
}

JdbcTemplate查询-queryForMap返回一个Map集合

API介绍

public Map<String, Object> queryForMap(String sql)

执行查询语句,将一条记录放到一个Map中。

使用步骤

创建JdbcTemplate对象

编写查询的SQL语句

使用JdbcTemplate对象的queryForMap方法

处理结果

public static void test04() throws Exception {
   String sql = "SELECT * FROM product WHERE pid=?;";
   JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
   Map<String, Object> map = jdbcTemplate.queryForMap(sql, 6);
   System.out.println(map);
}

JdbcTemplate查询-queryForList返回一个List集合

目标

能够掌握JdbcTemplate中queryForList方法的使用

讲解

org.springframework.jdbc.core.JdbcTemplate类方便执行SQL语句

API介绍

public List<Map<String, Object>> queryForList(String sql)

执行查询语句,返回一个List集合,List中存放的是Map类型的数据。

使用步骤

创建JdbcTemplate对象

编写查询的SQL语句

使用JdbcTemplate对象的queryForList方法

处理结果

public static void test05() throws Exception {
   String sql = "SELECT * FROM product WHERE pid<?;";
   JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
   List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, 8);
   for (Map<String, Object> map : list) {
      System.out.println(map);
   }
}

queryForList方法的作用?将返回的一条记录保存在Map集合中,多条记录对应多个Map,多个Map存储到List集合中

JdbcTemplate查询-RowMapper返回自定义对象

org.springframework.jdbc.core.JdbcTemplate类方便执行SQL语句

API介绍

public <T> List<T> query(String sql, RowMapper<T> rowMapper)

执行查询语句,返回一个List集合,List中存放的是RowMapper指定类型的数据。

使用步骤

定义Product类

创建JdbcTemplate对象

编写查询的SQL语句

使用JdbcTemplate对象的query方法,并传入RowMapper匿名内部类

在匿名内部类中将结果集中的一行记录转成一个Product对象

案例代码

// query使用rowMap做映射返回一个对象
public static void test06() throws Exception {
    JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
   // 查询数据的SQL语句
   String sql = "SELECT * FROM product;";
   List<Product> query = jdbcTemplate.query(sql, new RowMapper<Product>() {
      @Override
      public Product mapRow(ResultSet arg0, int arg1) throws SQLException {
         Product p = new Product();
         p.setPid(arg0.getInt("pid"));
         p.setPname(arg0.getString("pname"));
         p.setPrice(arg0.getDouble("price"));
         return p;
      }
   });
   for (Product product : query) {
      System.out.println(product);
   }
}

使用JdbcTemplate对象的query方法,并传入RowMapper匿名内部类

在匿名内部类中将结果集中的一行记录转成一个Product对象

JdbcTemplate查询-BeanPropertyRowMapper返回自定义对象

org.springframework.jdbc.core.JdbcTemplate类方便执行SQL语句

API介绍

public <T> List<T> query(String sql, RowMapper<T> rowMapper)

执行查询语句,返回一个List集合,List中存放的是RowMapper指定类型的数据。

public class BeanPropertyRowMapper<T> implements RowMapper<T>

BeanPropertyRowMapper类实现了RowMapper接口

使用步骤

定义Product类

创建JdbcTemplate对象

编写查询的SQL语句

使用JdbcTemplate对象的query方法,并传入BeanPropertyRowMapper对象

// query使用BeanPropertyRowMapper做映射返回对象
public static void test07() throws Exception {
    JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());
    // 查询数据的SQL语句
    String sql = "SELECT * FROM product;";
    List<Product> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Product.class));
    for (Product product : list) {
        System.out.println(product);
    }
}

BeanUtils.populate的用法

BeanUtils位于org.apache.commons.beanutils.BeanUtils下面,其方法populate的作用解释如下:

完整方法:

BeanUtils.populate( Object bean, Map properties )

这个方法会遍历map<key, value>中的key,如果bean中有这个属性,就把这个key对应的value值赋给bean的属性。

BeanPropertyRowMapper

使用BeanPropertyRowMapper将数据库查询结果转换为Java类对象。

一、应用

常应用于使用Spring的JdbcTemplate查询数据库,获取List结果列表,数据库表字段和实体类自动对应。

示例:

@Override
public List<Demo> findAll() {
    String sql = "SELECT * FROM user";
    /**
     * BeanPropertyRowMapper将查询结果转换为类对象
     */
    return jdbcTemplate.query(sql, new BeanPropertyRowMapper(Demo.class));
}
@Override
public List<Demo> selectUser(int uid) {
    String sql = "SELECT * FROM user WHERE id = ?";
    /**
     * 带条件查询
     */
    return jdbcTemplate.query(sql, new Object[]{uid}, new BeanPropertyRowMapper(Demo.class));
}

二、内部实现浅析

BeanPropertyRowMapper的initialize内部实现

如上图红框中所示:

image.png

mapperFields是一个HashMap,用来匹配Java对象的属性和MySQL表的字段名的。mapperFields中存放的是所有可能与MySQL表的字段名映射上的那些Java属性名字。

this.mappedFields.put(this.lowCaseName(pd.getName()),pd);

在initialize方法中,BeanPropertyRowMapper会把传入的泛型Java类的所有属性名称的全小写形式放入mapperFields中,

String underscoredName = this.underscoreName(pd.getName());

将Java类的属性名转化成下划线分割的形式,如userName会被转化成user_name,这是因为:

数据库设计字段名称,一般会使用下划线分割形式,如:user_name

而Java类设置属性,一般使用驼峰命名形式,如:userName。

使用BeanPropertyRowMapper自动绑定,需要确保数据库表列名称与Java实体类属性名称相同

Java集合-Map

什么是Map

1.不同于List单列的线性结构,Map提供的是一种双列映射的存储集合,它能够提供一对一的数据处理能力,双列中的第一列我们称为key,第二列就是value,一个key只能够在一个Map中出现最多一次,通过一个key能够获取Map中唯一一个与之对应的value值,正是它的这种一对一映射的数据处理关系,在实际应用中可以通过一个key快速定位到对应的value。综合上面的概念,可以概括出以下几个核心点:

2.Map存储是以k-v键值对的方式进行存储的,是双列的

Map中的key具有唯一性,不可重复

每个key对应的value值是唯一的

3.Java中Map是一个接口,它不继承任何其他的接口,可以说它是java中所有Map的顶级父接口。它的设计理念完全遵循上面的规则,只是具体的实现类种类很多,对应不同应用场景的使用,所以可能具体细节以及设计上存在差异。

Java的Map中提供了三种Map视图以便于展示Map中的内容:

1.只包含key的Set集合

2.只包含value的Collection

3.同时包含key-value映射的EntrySet

另外需要额外注意:不能使用可变的对象作为Map的key,因为一旦该对象出现变化它会导致Map的行为无法预期(这里的变化指的是影响equals方法比较结果的变化);同时不能将Map本身作为一个Map的key,但是允许将Map本身作为value存入Map结构中。

使用Map的时机

1.存储双列结果

有很多情况下我们都需要将数据梳理成双列的结果集存储起来,最常见的就是当查询数据库时,它返回的结果集中,对应字段名key和记录值value就是一个map,当然如果是列表查询,还会在Map的基础上包装一层List,但是它的每一条记录结果的表示形式就是借助Map来存储的,再比如在数据接收时,如果没有合适的对象接收时,可以考虑使用Map进行接收,最常见的就是前端传入json字符串,后端使用Map来接收数据,但是现在基本都采用JSONObject的方式来接收了,但是Map也是可以作为一个备用选项,在没有其他第三方插件可用的情况下,可以考虑使用Map,或者String接收,然后转成Map。

2.快速定位数据

因为Map的一对一映射的数据关系,利用这一特性,可以快速定位具体数据,现在的一些缓存操作就是利用的这一特点,我们将数据以Map的形式存储在内存中,在缓存的众多数据当中,未来如果需要获取数据时只需要给一个指定的key,可以快速定位到缓存的内容,而不必像List结构那样,需要记住具体的位置才能快速定位,当然如果能够确切记得元素位置,也可以使用List,而且效率更高,但是更多时候是不现实的,我们需要记住每一个元素在List中的位值,数据过多时就比较麻烦了,而且写出来的程序可读性也很差,因为只是通过整型的Index获取,而Map的key可以是任何类型,完全可以定义一个具有明确意义的内容。

3.需要唯一性存储的时候

因为Map的key具有唯一性的特点,我们完全可以利用这一特点作为一个“变异版”的Set来使用,我们知道Set的特点就是不可重复,实际上在Java中,HashSet确实就是这么干的,它将存入的元素放入一个HashMap(Map的一种实现)的key中,而Map中所有的value都是一个固定的Object类型的PRESENT常量,因为它的key不可冗余的特性正好符合了Set的特点,所以在HashSet的底层实现就依托于HashMap,而且Map本身也是无需的,注意:这里的“无序”是“不保证有序”,而不是“保证无序”,这两个概念是有区别的,前者说明结果可能会有序,也可能无序,不能保证;而后者说明结果一定是无序的。所以有时可以发现在遍历HashSet时竟然是有序的,这其实并不冲突。

Map的实现原理

因为Java中Map只是一个接口,它只是定义了一些方法以及规范,所以提到实现原理,还得结合具体的实现类来说明,而且Map的不同实现类,实现的原理也有所不同;Map的实现类很多,就以我当前使用的Java 10.0.2为例,Map接口的实现类就达到了31个之多(包括抽象类),这还只是JDK内部的实现类,不包括第三方库的实现。所以这里仅仅以几个常见的Map来详细说明,也是面试中经常被提及的几个Map类型:HashMap、HashTable、ConcurrentHashMap、LinkedHashMap等。

HashMap

概念

它是基于Hash表对Map接口的实现,所谓Hash其实就是散列,这里只需要记住:散列就是将一段变长的数据转换成一个固定长度的数据,这个过程称之为散列。在这个过程中会有一系列的算法计算,这里暂不深究;而Java获取对象的hash值比较方便,因为Object类中定义了一个hashCode方法,所以Java中的任何类都有直接或间接继承了hashCode方法,而HashMap就是根据key的hash散列结果来具体定位Map中的元素的;另外在HashMap中是可以使用null键和null值的;而且HashMap不是线程安全的;如果抛去这两点来看,HashMap其实与HashTable大致是相同的。它不能保证映射的顺序恒久不变,即:无法保证有序性;它的容量和加载因子紧密关系着它的迭代性能。

实现原理

整体设计概念上来说,HashMap采用的是数组加链表的方式来存储元素,因为hash可能存在冲突的问题,所以增加链表来存储hash值相同的元素,这种解决hash冲突的方法也叫链地址法。

首先,如果要深入了解HashMap,就必须明白以下四个概念:

capacity:它是指HashMap的容量,表示的是当前HashMap中最大可以存储的数据个数。

size:它反映的是当前HashMap中已经存放的元素个数

loadFactor:加载因子,它表示当前HashMap中的装满程度,默认为0.75,,它跟threshold计算有关

threshold:表示临界值,因为HashMap的容量不是一层不变的,当达到一定程度,就需要对HashMap进行扩容,而这个扩容的临界值就是用threshold表示,它的计算方式是capacity*loadFactor;因为加载因子默认是0.75,也就是说达到最大容量的四分之三时,再往里面加元素,就会扩容。

上面只是一个大致的介绍,下面就来逐步深入介绍相关的设计原理。

hash冲突

hash本身的意思就是散列,它的作用前面也介绍过,就是将一个变长的数据散列成一个固定长度的数据,在Java中,散列的结果是一个int整形数,也就是说hash的结果最大也不会超过Java中整型的最大值,任何对象都具有hashCode方法,我们可以重写hashCode方法,但是对象的数量是海量的,但是hash值就那么多,所以必然存在一种情况:hash值相同但是对象不同,这种现象就是hash冲突。在Java中如果是比较两个对象是否相同也是有策略的,首先会比较两个对象的hashCode方法,hashCode不同,对象肯定不同,这种判断可以隔绝大多数的比较了,而当hashCode相同时,会再去调用equals方法比较,如果equals也为true,这才能说明两个对象是相等的。所以equals为true的对象,hashCode一定是相等的,反之则不然。

HashMap的内部存储容器

map中最基础的存储结构就是数组,具体在HashMap中就是一个Node<K, V>类型的数组table(即hash表),这是一个可以存储键值对的数据类型组成的数组,我们放入map中的元素全部都被存入到这个数组中,而map容量的扩充实际上也就是对这个数组的不断变更。

HashMap的初始容量设计

使用HashMap的时候,最常用的就是无参的构造方法,然后调用put方法放入键值对,这种方式采用的是默认的初始化策略,此时当我们刚刚new出一个HashMap对象时,它的内部table数组实际上是一个null(我这里是以java 10.0.2为版本介绍的,其他版本可能略有不同)。只有当我们第一次调用put时才会进行table的初始化。此时它的capacity会被设置为DEFAULT_INITIAL_CAPACITY = 1 << 4,也就是16。加载因子loadFactor是默认的0.75,所以这时候它的threshold是12。

但是我们在创建HashMap时也可以指定capacity甚至是loadFactor,这里一般不推荐修改loadFactor:

//DEFAULT_LOAD_FACTOR = 0.75f

public HashMap(int initialCapacity) {

 this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

需要注意的是:它传入的initialCapacity并不是最终我们构建的HashMap对象的容器大小,它会经过一次运算,得到一个比initialCapacity值稍大的数值,并且一定是离initialCapacity最近的那个2的N次幂的值,比如:我们传入5,它就会找到8,传入9,就会找到16。也就是说最终得到的capacity的值一定是2的某个次幂。这个过程是需要计算的,JDK中采用是一种比较高效的位移运算,具体如下:

首先来看几个简单示例:

5   0000 0000 0000 0101      |      19  0000 0000 0001 0011

7   0000 0000 0000 0111      |      31  0000 0000 0001 1111

8   0000 0000 0000 1000      |      32  0000 0000 0010 0000

—————————————————————————————————

9   0000 0000 0000 1001      |      37  0000 0000 0010 0101

15  0000 0000 0000 1111      |      63  0000 0000 0011 1111

16  0000 0000 0001 0000      |      64  0000 0000 0100 0000

根据上面的数据展示,可以看到如果我们想要将5最终变成离它最近的那个8,需经历:5 — 7 — 8这么一个过程,也就是将它的原本二进制数据有效部分全部转换成1,然后在此基础上加1就可以得到目标值,同理9到16,19到32等也是如此,而JDK中HashMap源码采用就是这种不断右移然后按位或运算,最终得到一个有效数据部分全为1的数值,然后加1得到结果,这样再来看HashMap的这部分计算源码就不会迷惑了:

static final int tableSizeFor(int cap) {

 int n = cap – 1;

 n |= n >>> 1;

 n |= n >>> 2;

 n |= n >>> 4;

 n |= n >>> 8;

 n |= n >>> 16;

 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

可以看到,最终计算出来的n与MAXIMUM_CAPACITY比较,只要比它小,就会加1,而在此之前的逻辑主要就是为了将对应的二进制位全部转换成1。这里有一点需要注意:按照上面介绍的逻辑会有有个情况,就是如果我们传入的数值本身就是2的次幂值,那就会有问题,因为此时传入的capacity本身就已经可以作为初始容量了,解决方式很简单,就是上面方法中的第一步操作,先将传入的capacity减1,再进行计算,此时如果传入的是8,那么减1之后,n其实是7,经过计算后正好是8,符合逻辑;那如果此时传入的是9,虽然减1后得到了8,但是此时需要的是更大数值的16,经过位移计算,正好得到15,加1后得到16,所以逻辑上也没有问题。

HashMap的存储

HashMap的存储方式就是利用了map中的key的hash值作为定位基础的,具体做法就是:我们在存入一个k-v键值对的时候,它会计算key的hash值,也就是调用key对象的hashCode方法,因为hashCode的取值范围是整个整型范围,所以可能会非常大,为了让它能够和table数组的下标index挂钩,这里就会将得到的hashCode值与table的长度取模,这样得到的数据肯定是在table.length之内的整数,然后用它作为数组对应的下标。注意:这里JDK采用了一定的优化,它的取模并不是常规的hash % table.length,它使用的是hash & (table.length – 1)这种按位与的操作,这么做有两个好处:

首先就是效率很高,比正常的取模运算符要快

避免了负数问题,结合前面的初始容量设计可以知道,table.length – 1得到的一定是一个正数值,所以它的最高位一定是0,而0与任何数按位与运算,得到的一定是0,此时无论hashCode值是整数还是负数,计算出来的结果一定是正数。

而为何会出现hash & (table.length – 1) == hash % table.length呢?其实想想也能明白:

经过前面分析可以得到table的长度必然是:table.length == 2^N,则hash % table.length实际上就是hash / table.length然后取余数,也就是hash >> N 位移运算过程中,移出的那部分数据即为需要的模运算结果。而table.length – 1的结果必定是一个二进制有效位全为1的数据(参考前面容量初始化设计),此时hash与减1后的值进行按位与,可以保证低位的结果保留,而超过table.length – 1数值的高位部分得0,这个结果正好就是前面位移运算过程中需要得到的那个移出的部分。

按照上面的介绍,最终可以根据key的hash得到一个对应的数组位置index,但是我们也知道hash是会冲突的,这里如果出现了冲突,经过计算后得到的index位置上已经有元素了怎么办,这时候链表结构就发生作用了,它会将最新添加的元素放在数组中,将该位置上之前的元素以链表的形式放在新加入的元素的后面,Node的设计本身就是一种链表存储格式:

static class Node<K,V> implements Map.Entry<K,V> {

 final int hash;

 final K key;

 V value;

 Node<K,V> next;

 …

}

所以:数组中的元素肯定是最新放入的元素,之前的老元素会按照链表的方式挂在最新元素的后面。这种存储就是数组加链表的存储方式。

需要注意的是:HashMap中使用到的hash值并非是对应key对象上调用hashCode方法,它又经历了一步计算:

static final int hash(Object key) {

 int h;

 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

这么做的目的也就是为了降低hash冲突,尽可能的让key的每一位数据都会影响到散列的结果,所以它会对hash进行扰动计算,防止hashCode的高位不同但低位相同导致的hash冲突,将hash的高位和低位的特征结合起来,共同影响hash结果,尽可能将每一位数据都与hash结果相关联,从而降低冲突。具体的计算,强烈推荐H大的博文:透彻分析hash()

随着数据的不断增多,冲突的可能性就会越来越大,极端情况就是链表部分会变得很长,而链表结构的增删的效率很高,但查询的效率很低,这样如果链表过长,会影响Map的查询性能,所以JDK1.8之后,对链表部分做了改动,一旦链表的长度超过了8个Node,就会直接转换成红黑树结构,检索的时间复杂度大大降低。

另外对于扩容方面,如果元素个数超过了capacity*threshold的值,容器就会调用resize方法扩容,扩容很简单:就是将原来的oldCapacity << 1,也就是直接扩大一倍,这样也保证了capacity的值始终都是2的指数值。

使用HashMap时机

总体上来说,HashMap的查找效率比较高,常见的应用场景中,需要使用Map的地方,HashMap足以解决大多数问题,而且它的效率也是很高的,如果数据量较大,这个效率提升还是很可观的,如果没有线程安全的要求,并且可以允许Key出现null值的情况下,可以考虑使用HashMap。

HashMap的问题

线程安全

观察HashMap的源码,可以看到,没有任何线程安全的概念,对于put,remove等操作方法,没有任何加锁、同步化的操作,所以它是线程不安全的,在多线程环境下,可以考虑如下方案:

使用HashTable,但是效率极低,它是方法加锁,加锁粒度很粗糙,性能较低

使用Collections工具类可以创建一个线程安全的HashMap,实际的结果跟前面一种类似,也是一种方法加锁,不推荐

可以考虑在自己的业务逻辑中自行实现同步逻辑,灵活性较高,但是会影响代码的整体阅读逻辑

可以使用ConcurrentHashMap,它是也是线程安全的map,引入了段加锁机制,效率比HashTable要高很多

另外,据网上有些博客提出:在并发状况下,多个线程同时对一个map进行put,可能会导致get死循环,具体可能表现为:

多线程put操作后,get操作导致死循环。

多线程put非NULL元素后,get操作得到NULL值。

多线程put操作,导致元素丢失。

但是这种情况我写了一些demo,暂时没有出现上面这种情况,可能是因为JDK版本的缘故,新版本的HashMap做了一定的优化,故暂时无法重现这个问题。

性能开销

hash函数的计算会影响这个性能,对于一些极端情况,例如对象比较复杂,hash的计算可能比较耗时,这部分性能的损耗也应该考虑在后续实际生产中,如果Map中存储的key比较复杂,就要考虑是否需要换一种存储方式了。

初始容量设置问题

我们在创建一个HashMap的时候,一般都是用默认的无参构造方法,这种方法虽然方便,但是性能上面可能并不是最符合我们当前正在运行的程序环境,所以一般都建议指定一个初始容量,这样可以尽可能保证减少map扩容带来的性能耗损问题,同时也减少了rehash(扩容后重新进行hash计算)的次数,对性能的提升也有一定好处。

但是初始容量的设计也是有讲究的,不能越大越好,要最符合当前的应用环境,当然前提是能够预知到map中到底会存储多少元素,否则默认的构造方法是最好的选择。在阿里巴巴的Java开发者手册中给出了一个计算初始容量的公式:

capacity = (要存储元素个数 / 加载因子loadFactor) + 1

//加载因子loadFactor默认0.75

其实这种计算方案在Google的Java开源工具库guava也有,最终的灵感来源还是JDK源码中HashMap的putAll方法得来的,它里面采用的就是这种计算方式,它这么设计是有一定的好处的:

例如现在需要加入7个元素,如果只是直接传入7,那么最终的HashMap容量会被设置为8,但是由于此时threshold = 8 * 0.75 = 6,所以在加入第七个元素的时候会有一个hash表扩容,这个是比较耗费资源的操作。而如果使用上面推荐的公式,可以计算最终传入的值为:7 / 0.75 + 1 = 10;那么最终计算后的capacity为16,在进行元素装入的时候就可以有效避免过于频繁的hash表扩容操作,从而提升性能。

其他

说到HashMap,这里顺便说一下HashSet,它在前面也稍微提到了一下,它是一种Set,Set的特性就是:无序,唯一。而HashSet的底层实现就是利用的HashMap的key唯一性特点,而且HashMap本身也是不保证有序的,所以正好与Set的特性不谋而合,所以HashSet在存储元素的时候,都是将元素存入HashMap的key中,value部分始终都是一个固定的常量Object对象,没有实际意义。

HashTable

首先在概念上,它基本上与HashMap没有太大区别,甚至于使用的方式都差不多,不同的只是它的底层设计上与HashMap存在一定的差异,而正是这些差异,使它的使用场景更加单一。另外:HashTable是不存能出null值的,无论是key还是value,这点与HashMap完全不同,而且它是线程安全的。

实现原理

它的整体设计思路基本和HashMap差不多,都是基于Node数组进行存储,这个Node实际上就是一个链表的节点类型,内部维护一个Node类型的数组,Node的结构设计基本都差不多,下面主要从几个与HashMap不同的地方说明以下具体的细节差异。

线程安全

首先明确一点,HashTable是线程安全的,看它的源码就知道了,它里面的所有涉及到更新HashTable的操作都被加了synchronized锁。

继承关系

与HashMap不同的是HashTable继承自Dictionary(HashMap继承自AbstractMap),这是一个非常古老的抽象类,它从java1.0的时候就已经存在了,现在连这个类的头部注释中都说这个类已经是obsolete(过时的)类了,对于现在的JDK而言,Dictionary所能达到的效果,Map接口都能达到,甚至还比它的灵活性更高(因为接口可以多实现,类只能单继承),所以官方推荐后续的实现都基于Map接口了。

迭代

HashTable的遍历使用的是枚举Enumeration,而不是我们常见的Iterator迭代器,例如我们如果调用它的keys方法,它会返回一个Enumeration类型的对象,这个查看一下源码就可以很清楚的看到。当然,它本身也是有Iterator的,Enumeration是因为历史遗留问题才一直存在的。它的Iterator迭代与HashMap都是支持fast-fail的,fail-fast这里就不再赘述,参考Java集合–List。

初始容量设计

它默认的初始容量为11,并非HashMap的16,但是它的默认加载因子loadFactor却仍然是0.75,官方采用0.75做加载因子其实也是经过时间空间的消耗权衡得到的结果,按照官方的注释解释:如果过高,虽然会减少空间上的开销,但是会增加查询上的时间成本,所以才说不建议修改loadFactor。

扩容设计

它的扩容不同于HashMap的直接翻倍,每次当容量达到threshold后,新的capacity = 2 * oldCapacity + 1。所以它的到的capacity值始终都是一个奇数,默认是从11开始的。另外它也没有对传入的capacity再计算的过程,它的源码中只有如下设计:

if (initialCapacity==0)

 initialCapacity = 1;

this.loadFactor = loadFactor;

table = new Entry<?,?>[initialCapacity];

threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);

可以看到,只是对initialCapacity进行了是否为0的判断,如果为0,默认赋值为1,否则capacity的值就是传入的值。所以我们在构建HashTable对象时,如果需要传入capacity,最好也按照它的设计初衷,传入一个奇素数。

hash计算

HashTable的hash计算就是直接调用key对象的hashCode方法,没有进行进一步扩散计算,而且它的取模运算也没有HashMap那种采用效率更高的&操作,据说这是跟HashTable的长度数值有关,当哈希表的大小为素数时,简单的取模,哈希的结果会更加均匀。没有具体深入研究过,有待探讨。

HashTable的问题

性能

它的性能问题是广为诟病的,因为它的synchronized锁都是加在方法上的,synchronized本身就是重量级锁,并且加锁粒度又很粗糙(方法级锁),我们在现实场景中,其实99%的情况可能都不会出现线程安全问题,所以仅仅为了那1%的并发安全去使用它多少有点浪费性能,完全可以自己控制同步,而且如果有选择,选择ConcurrentHashMap也是一种不错的方案。只有在一些特殊的应用场景中可能会采用HashTable存储数据。

并发迭代修改

虽然HashTable被描述为线程安全的,似乎在多线程环境下就不存在任何问题了,但是仍需注意,如果我们使用迭代器对HashTable进行遍历的时候,它采用的是fail-fast机制,所以仍然有可能抛出ConcurrentModificationException异常。另外HashTable的另一种迭代方式:Enumeration,它是不支持fail-fast的,所以如果有需要检测这种并发修改迭代的情况,Iterator是唯一的选择。

ConcurrentHashMap

原理

因为HashTable的加锁粒度过大,所以JDK1.5以后出现了这个同样支持并发操作的Map结构,ConcurrentHashMap引入了Segment(段)的机制,所谓段,就是将Map的容器进行分段(也就是常说的“桶”),每段单独加锁,所以当并发修改时,如果不是同时操作同一个段内的数据,段与段之间是互不影响的,这就是所谓的锁分段技术,正是因为段与段之间是独立的加锁,所以大大提升了并发性能。

对于java6和java7的版本中,主要就是segment机制的引入,内部拥有一个Entry数组,数组中每个元素又是一个链表结构,但是同时也是一个ReentrantLock(Segment继承了ReentrantLock)。

而Java8以后,ConcurrentHashMap做了很大的改动,有些甚至是颠覆性的,它摒弃了之前一直使用的Segment机制,虽然源码中仍然存在该类,但是源码的注释中已经说明了它是一个unused(无用的)类,它只是用来兼容之前版本的ConcurrentHashMap。新版本的ConcurrentHashMap采用了CAS算法,无锁化的操作大大提高了性能,底层仍然采用HashMap的那套实现,即:“数组+链表+红黑树”的方式。同时为了做到并发,也增加了一些辅助类,如:TreeBin、Traverser等内部类。

继承关系

ConcurrentHashMap除了和HashMap一样继承了AbstractMap,同时它也实现了一个叫ConcurrentMap的接口,这个接口的作用主要是为Map提供线程安全以及原子性的保证。另外不同于HashMap,它没有实现Cloneable接口,所以如果涉及对象复制,需要额外考虑其他方式。其他的基本都与HashMap一致了。

初始化容量和扩容机制

对于初始容量的设置,默认加载因子以及扩容的方式,ConcurrentHashMap采用的方案与HashMap的机制是一模一样,所以对于HashMap的那一套在这里是通用的,它的内部结构也是一个Node数组,并且它的Node类的内部定义几乎与HashMap的一致,不同的是此时的Node中代表Value的val和指向下一个Node节点的引用next是volatile修饰的,这个也是为了在高并发情况下,为不同线程间数据可见性而考虑的。而且不仅仅是这两个字段,在整个类中,除去静态常量,其余的变量几乎全部都用volatile修饰。

如果在创建ConcurrentHashMap对象时,我们手动传入了capacity值,这里它不是像HashMap那样直接对传入的capacity值进行计算求取最近的2的指数值,而是会将传入的initialCapacity进行如下运算:

tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)

而这里的tableSizeFor方法就是根据传入的值,对capacity进行不断位移以及按位或的操作,最终求出一个合适的2的指数值。即:如果创建ConcurrentHashMap对象时,如果指定了capacity,在实际创建最大容量时,会先对传入的capacity扩大3倍并加1,再根据此时的值再此进行求取最近的不小于该值的2的指数幂值。

几个重要的属性

前面HashMap中介绍过的几个重要属性这里就不再重复了,这里就重点提一下sizeCtl属性,它的作用很大,用途比较多,根据不同的值,可以分为以下几种情况:

负数代表正在进行初始化或扩容操作

-1代表正在初始化

-N表示当前有N – 1个线程正在进行扩容

整数或0代表hash还没有被初始化,此时它的值表示的是初始化大小或者是下一次扩容的大小,有点类似于前面介绍过的threshold(阈值)的概念,它的值始终是当前ConcurrentHashMap容量的0.75倍,与loadFactor正好相对应。

//下面两个值主要是用于与hash值进行比较时使用,判断当前节点类型

static final int MOVED     = -1; // hash值是-1,表示这是一个forwardNode节点

static final int TREEBIN   = -2; // hash值是-2  表示这时一个TreeBin节点</pre>

MOVED和TREEBIN在容器扩容,遍历以及存放元素的时候,有很重要的作用。

核心类

Node

跟HashMap一样,它是包装了K-V键值对的类,前面说过,它的整体设计思路跟HashMap几乎一样,只是它的value和next属性使用了volatile修饰,保证了在并发环境下线程间的可见性,同时比较有意思的是它的setValue方法:

public final V setValue(V value) {

 throw new UnsupportedOperationException();

}

可以看到,它不允许直接使用setValue方法对Node中的value属性进行修改,如果这么做,会直接抛出异常。这个方法在HashMap中是可以正常使用的。同时,相较于HashMap,它又新增了一个find方法,注释上解释它的功能主要是为了辅助map.get方法,在子类中会进行重写。

TreeNode

当Node的链表结构过长时(一般是为8个元素),HashMap就是将其转换成红黑树形式,而转换的基础就是直接借助TreeNode,但是ConcurrentHashMap中却不是直接使用TreeNode,它是将这些TreeNode节点放在TreeBin对象中,然后由TreeBin完成红黑树的包装,而且这里的TreeNode是继承自ConcurrentHashMap中的Node类。

TreeBin

TreeBin的作用很明确,它的构造函数就一个,接收的参数就是TreeNode,它对TreeNode进行包装,甚至于当链表转换成树结构的时候,哪怕它的根节点也是TreeBin,并非HashMap中的TreeNode,所以可以说:ConcurrentHashMap中,如果数组中某个数组位置上的结构转变成了树结构,那么存储在数组中的实际元素就是TreeBin对象。而对于其他没有转换成树结构的位置(链表长度仍然在8个以内),仍然是原本的Node类型。

ForwardingNode

一个用于连接两个table的节点类,这个类在进行扩容的时候有很大的作用,它内部包含了一个nextTable引用,类型是Node类型,而且它的key、value以及next都是null,hash值为-1,后面在介绍扩容的时候会说道它的作用。

CAS无锁化

UnSafe静态代码块

在我当前使用的java10.0.2版本中,源码的6373行到6403行这段代码是无锁化的静态语句块部分,它里面利用了jdk.internal.misc.Unsafe类对一些重要属性的修改采用CAS操作,大大提高了程序的性能,因为没有锁的开销问题,许多锁带来的问题也就不存在了。

三个无锁化的核心方法

//获取tab数组中i位置上的Node

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {

 return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);

}

//利用CAS设置tab数组中i位置上的Node节点,这里就是典型的CAS操作,设置值的时候,传入待修改的值v和比较值c

//在修改时,将c的值与内存中的值比较,只有相等时,才将该位置上的Node设置为传入的v,否则修改失败

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,

 Node<K,V> c, Node<K,V> v) {

 return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);

}

//顾名思义,这个方法就是利用CAS操作,将tab数组中的i位置设置为传入的v

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {

 U.putObjectRelease(tab, ((long)i << ASHIFT) + ABASE, v);

}

正是有了上面这三个核心操作,才保证了ConcurrentHashMap的线程安全的特性。

初始化initTable

在构建ConcurrentHashMap对象的时候,它的构造方法采用的策略和HashMap中的大致是一样,在构造方法中其实并没有对具体的存储数组进行指定,只是简单初始化了一些必要的参数指标,具体的table的初始化都是放在插入元素时进行的,在插入前会对table进行null值判断。

private final Node<K,V>[] initTable() {

  Node<K,V>[] tab; int sc;

  while ((tab = table) == null || tab.length == 0) {

    //根据前面的介绍,此时sizeCtl若小于0,表示有其他线程正在进行初始化操作

    //此时当前线程放弃初始化,自旋直至其他线程初始化结束

    if ((sc = sizeCtl) < 0)

      Thread.yield(); 

    else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {

      //此时表示当前线程得到了初始化权限,此时将sizeCtl设置为-1,阻止其他线程进入

      try {

        if ((tab = table) == null || tab.length == 0) {

          //DEFAULT_CAPACITY = 16

          int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

          @SuppressWarnings("unchecked")

          Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

          table = tab = nt;

          //此时n是当前table容器的大小,n>>>2实际上就等于0.25n

          //所以sc = 0.75n

          sc = n – (n >>> 2);

        }

      } finally {

//修改sizeCtl的值为当前容器大小的0.75倍值

        sizeCtl = sc;

      }

      break;

    }

}

return tab;

}

扩容transfer方法

由于这个方法过长,这里就不再贴它的源码了,强烈建议琢磨一下源代码,它里面的涉及到的设计思路还是比较精妙的,它的大致扩容思路是这样的:

开始的时候它会进行一次CPU的可用线程的计算,根据可用线程数目和 Map 数组的长度,平均分配处理当前待扩容的“桶”。默认的是每个线程需要处理16个“桶”,所以换句话说,如果当前map的容量为16的时候,那么扩容阶段就只有一个线程在扩容。

它在扩容时需要用到一个额外的数组空间nextTable,它的大小是原table的两倍,一旦扩容完成,原来的table就会被新的nextTable所取代。

扩容后,就是将原来数组中的内容复制到新的nextTable数组中去,这里的复制转移并不是简单的copy,它是有策略的

通过前面的分析我们知道,数组中的元素存在不同的情况,既有Node(此时说明是链表结构),也会有TreeBin(链表过长转换成了红黑树)以及null(这部分是还未存放元素的“桶”)。

首先在进行遍历复制的时候,原数组中null的位置在新的数组中同样位置上会插入一个占位符forwarNode节点,当其他线程在遍历到当前节点是forwardNode类型,就直接跳过该节点,继续向后遍历。

剩下的链表和红黑树结构中,在复制到新的数组中时会对其进行拆分,拆分的规则是将当前节点的hash值与length进行取余操作,假如在原数组中的位置是index,那么根据取余的结果,如果为0,就在新数组的index位置防止,否则不为0的话,就将其放在index+n的位置,这里的n一般就是16。

这样原来数组中的链表和红黑树部分就都各自被拆分成了两份存储在新的数组中,原来的null位置依然为null,没有任何变化,这样就完成了数组的扩容操作。下面一份关于扩容的示意图:

首先我们现在假设当前map的容量为16:

concurrentHashMap01.png

其余数组中的内容暂时不用考虑,单独看这里的index为1和4的位置上的内容,假设其他位置上的内容都是null,那么扩容后,数组的容量就会变成32,然后1位置上的蓝色节点会组成一个新的链表,放在新数组中的1位置上,而1位置上的黄色节点会组成新的链表放在新数组的17位置上。同样的4位置上此时链表长高度达到8,应为树结构,但是为了方便表示,这里也将其画成了链表结构,在拆分后,蓝色黄色节点各自组成新的链表,且长度减到了4,重新变成了链表结构,如果拆分后链表长度仍然过长,扩容后仍然会保持红黑树结构。

concurrentHashMap02.png

put方法存放元素

首先需要明确的是,ConcurrentHashMap中put是不能存放key或者value为null的元素的,否则会直接抛出空指针异常,这一点有别于HashMap。另外因为ConcurrentHashMap可以运行于多线程环境中,所以它的put方法逻辑比较复杂,简单来说,它的put方法的主要逻辑就是:

首先根据传入的key计算对应的table中的位置index

判断table[index]当前位置中是否存在元素,如果没有元素,直接放入,没有加锁操作

如果当前位置上已经存在元素了,节点上锁,然后依次遍历链表节点,如果遇到了key和hash都是一致的元素,就更新这个位置上的value,注意这里的上锁的节点可以理解为hash值相同组成的链表的头结点。

如果一致遍历到节点链的末尾,都没有找到key和hash都相同的元素,那么就可以认为它是一个插入操作,此时就把这个节点插入到链表末尾。

如果table[index]位置上的内容为树节点,就按照树的方式是插入节点

在插入结束后,如果是链表结构,需要判断当前链表长度是否达到了8,如果是,还需要将其转换成红黑树。

最后同步更新一下当前map中的元素数量

可以看到,新版本的ConcurrentHashMap中,put时锁住的是Node节点,而不是像之前JDK1.6和1.7那样锁住整个segment。而且在锁住Node之前的操作也是线程安全的,它的线程安全就依托于前面介绍过的三个核心的CAS方法。

size属性

因为可能存在高并发的情况,所以不像HashMap那样直接调用size方法就可以准确获取当前map中的元素个数,在高并发下,可能存在当我们需要获取size值的时候,其他线程正在往map中写数据,我们不能像虚拟机的垃圾回收一样,在统计时“Stop The World”,所以得到的size值其实并不是一个精确的值。对于这个大概的size值的获取,ConcurrentHashMap也是利用了一些策略才得到的,并非直接返回size属性值。

辅助的内部类和变量

@jdk.internal.vm.annotation.Contended static final class CounterCell {

 volatile long value;

 CounterCell(long x) { value = x; }

}

//它是一个基于计数器的值,其实也就是存放的是map中的元素个数,使用CAS更新

//但是它并不是强制的等于当前map中元素的个数

private transient volatile long baseCount;

//当调整大小、创建CounterCells时用于CAS自旋锁操作

private transient volatile int cellsBusy;

//计数表,当非空的时候,它的容量是2的幂

private transient volatile CounterCell[] counterCells;

size方法

public int size() {

 long n = sumCount();

 return ((n < 0L) ? 0 :

 (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :

 (int)n);

}

//JDK1.8开始新增的一个方法,它与size方法很类似,而且从它的注释上来看,很推荐使用它来代替size方法

public long mappingCount() {

 long n = sumCount();

 return (n < 0L) ? 0L : n; // ignore transient negative values

}

//无论是size还是mappingCount,都没有直接返回baseCount的值

//而是在baseCount的基础上继续进行了计算,即便如此,它得到的仍然是一个估计值

final long sumCount() {

   CounterCell[] as = counterCells; CounterCell a;

   long sum = baseCount;

   if (as != null) {

     for (int i = 0; i < as.length; ++i) {

       if ((a = as[i]) != null)

         sum += a.value;

       }

     }

   return sum;

}

addCount方法

前面在介绍put方法的时候,在最后一步说到“同步更新一下当前map中的元素数量”,这句话在put方法的代码中其实就是调用addCount方法,而这个方法会做两件事情:利用CAS对baseCount的值进行更新,然后判断此时是否需要扩容。

ConcurrentHashMap的问题

size的准确性

上面说过,它的size方法得到的值并不是一个准确的值,但是在大多数情况下,这个值是可以保证的,只有在极端并发环境下才有可能出现不一致的情况,所以我们在使用时,不能依赖于size方法来确定map中精确的元素个数,应当有适当误差。并且现在也更加推荐了mappingCount方法来代替size方法使用。

数据覆盖问题

当我们在高并发环境下,纯粹put或者get操作,其实是没有问题的,但是当我们调用get之后,在下次调用put方法之前,如果有其他线程也对该map调用了put方法,那么后续在调用put方法的时候,就有可能把刚才其他线程填入的值覆盖掉,即使ConcurrentHashMap中使用CAS操作,仍然不可能完全避免这种情况。但是这中属于具体编码问题,控制合理的话,编码人员可以避免这样做,只要稍加注意,可以采用一些额外的手段保证它的一致性。

空值问题

一定不能使用ConcurrentHashMap的put方法放入一个key或value为null的元素,否者直接NullPointerException。

LinkedHashMap

原理

可以看以下它的定义:

public class LinkedHashMap<K,V>

 extends HashMap<K,V>

 implements Map<K,V>

从它的定义中可以明显的看出,它就是HashMap的子类,所以它的一切都是基于HashMap的,查看它的源代码可以看到,它的初始化,扩容,元素的存放与获取底部都是基于HashMap的那一套原理,所以这里就不再继续介绍了,但是它有一个HashMap没有的特点,就是双向链表,简单来说就是:它的本身的存储结构依然采用HashMap的那套数组加链表的方式,但是它的节点内部又多维护了两个引用before和after,before指向当前元素之前放入的元素,next指向紧接着放入的元素引用,所以它们的引用关系与放入Map中的元素顺序有关,也就是说它除了之前介绍的每个“桶”内部的链表结构,桶与桶之间的不同节点也有一个引用在维护,并且是双向的链表结构,这样整体的感觉就是:

LinkedHashMap01.png

概括起来就是:它的主体存储结构仍然是HashMap的那套逻辑,只是在HashMap的基础上,每个节点又多维护了一份之前存入的元素引用以及之后存入的元素引用,这样每个节点内部算起来实际上有三个引用(HashMap本身就有一个链表引用,主要是hash值相同的元素之间的引用,然后又有了这个新增的两个引用)。

LinkedHashMap02.png

特点

从它的结构可以看出,HashMap所拥有的特性它都是存在的,同时因为加入了双向链表的维护,所以它是一个有序的Map,前面说过,HashMap是不保证有序的,也就是它遍历的结果可能与它放入的顺序不是一致的(不保证结果有序性,并不是一定是无序的),而LinkedHashMap的结果必定是有序的,所以如果需要使用有序的Map场景,可以考虑使用LinkedHashMap。

使用场景

前面介绍过的Map的使用时机这里都完全可以适用于LinkedHashMap,此外它还可以保证结果的有序性,所以如果对遍历的有序性有要求,可以使用它。

另外:它还可以用来实现LRU(Least recently used, 最近最少使用)算法,这是因为它内部有一个removeEldestEntry方法,这个方法的返回值为boolean类型,如果我们需要实现LRU,可以继承自LinkedHashMap,重写它的方法,此时,如果需要使用实现LRU,它有一个属性必须设置为true,那就是:accessOrder,只有它为true的时候,双向链表中的元素就会按照访问先后顺序排列。这里有一个简单使用的例子:

public class LRU<K, V> extends LinkedHashMap<K, V> implements Map<K, V> {

   public LRU(int initialCapacity, float loadFactor, boolean accessOrder) {

   super(initialCapacity, loadFactor, accessOrder);

 }

 @Override

 protected boolean removeEldestEntry(Entry<K, V> eldest) {

   //保证map中元素个数

   return size() > 6;

 }

 public static void main(String[] args) {

   LRU<Character, Integer> lru = new LRU<Character, Integer>(

     16, 0.75f, true);

   String s = "abcdefghijk";

   for (int i = 0, len = s.length(); i < len; i++) {

     lru.put(s.charAt(i), i);

   }

   System.out.println(lru); //{f=5, g=6, h=7, i=8, j=9, k=10}

 }

}

最终得到的结果可以看到,map中始终都是只包含6个元素,如果过多,之前插入的节点都会被抛弃,抛弃的都是最近最少使用的节点。

存在的问题

总体来说,前面说道到的HashMap的存在的问题在这里仍然是存在的,它不是线程安全的,而且它的存储开销比HashMap更大,这个也好理解,毕竟它又多了一个双向链表的维护,所以无论是复杂度还是维护成本都会高一些。

ConcurrentSkipListMap

介绍

最后再来看一个基于跳表的数据结构,它是一个支持排序和并发的Map,是线程安全的,这种跳表的结构我们正常开发中很少用到,所以对于我而言,它是一个知识盲点,下面就简单介绍一下这个跳表。

原理

在介绍跳表之前,首先需要知道传统的链表结构是什么样子,传统的链表是一个线性结构,如果我们需要查询某个元素,需要从链表的头部挨个遍历访问,然后找出目标元素。所以,链表结构的查询需要O(n)的时间,它与链表长度紧密相关。而跳表就是基于传统链表上做的一次改进,它是采用空间换时间的方式来提高查询效率,这里以一个最简单的方式来描述一下它的数据结构,实际情况中它的结构会比下面的图示复杂一点,后面会介绍:

SkipListMap1.png

上面的结构就是最简单的一种跳表的结构,首先需要明确的是:跳表天然是有序的,所以上面的链表结构是有序的。它除了正常的链表结构(有一个引用指向下一个节点)外,每个节点多了一个引用,用于指向下下个节点,这里,如果我们查询一个值为12的元素,具体查询步骤就是:

SkipListMap2.png

首先12与1比较,比1大,向后找到6节点

12与6比较,比6节点大,向后找到9节点

12与9比较,比9节点大,向后找到17节点,说明12在9节点与17节点之间

12与9比较,比9节点大,向后找到12节点,即为目标节点

可以看到,它的查询是跳跃式进行的,跳过了中间的3和7节点,所以它查询的时间复杂度为O(n/2)。但是前面也说过了,这只是最简单的一种方式,实际情况中,一个链表节点的内部可能包含的不仅仅只有两个引用(下一个节点+下下个节点),每个节点内部到底会带有多少个后续节点的引用是随机生成的,所以实际情况可能是这样:

SkipListMap3.png

每个节点中拥有的后续节点的引用个数不定,这是一种概率均衡技术,而不是强制性均衡约束,所以对于节点的插入和删除比传统的平衡树算法更为简洁高效。

查找

SkipListMap4.png

例如查找值为12的元素,就会按照红色箭头上的数字步骤查询即可,这里就不再赘述了。

插入

找到需要插入的位置

申请新的节点

调整指针

SkipListMap5.png

上图的解释如下:

假设我们这里需要插入一个值为15的节点,最后找到的位置就是上图中红色圆圈的位置,然后申请新的节点,将节点调整指针,放入12的后面即可,这里有一个技巧:使用一个update数组保存将要插入位置之前的节点直接引用,这里就是上图中红色线框框住的三个引用,因为在插入节点时,只有这三个引用可能涉及到指针的调整。调整后的情况即为:

SkipListMap6.png

删除

删除操作其实和插入很类似,找到节点,将其删除,然后调整指针即完成整个删除操作,插入逻辑中用到的update数组技巧在这里仍然适用。

Java中的ConcurrentSkipListMap实现

在java中,跳跃表是具有层级结构的,即所谓的level,整体结构大致如下:

SkipListMap7.png

跳表的结构具备以下特征:

最底层包含所有节点的一个有序的链表

每一层都是一个有序的链表

每个节点都有两个指针,一个指向右侧节点(没有则为空),一个指向下层节点(没有则为空)

必备一个头节点指向最高层的第一个节点,通过它可以遍历整张表,如上图中的左上角的蓝色节点BASE_HEADER

在新增节点时,假设此时我们加入一个值为80的节点,它是通过如下步骤来添加的:

首先,它会将80节点加入level1中的链表中

SkipListMap8.png

根据概率算法,计算处一个level值,这里假设为4,然后根据跳表算法描述,构建新的索引

SkipListMap9.png

将各个索引层次上的节点连接

SkipListMap10.png

其他

目前常用的key-value数据结构有三种:Hash表、红黑树以及SkipList,它们各自有着不同的优缺点(不考虑删除操作):

Hash表:插入、查找最快,为O(1),数据的有序化需要显式的排序操作

红黑树:插入、查找为O(logN),但是常数项较小,如果采用无锁化实现,复杂度很高,一般需要加锁,数据天然有序

SkipList:插入、查找为O(n),常数项比红黑树要大,底层结构为链表,可无锁实现,数据天然有序

java8 方法引用

https://www.cnblogs.com/xiaoxi/p/7099667.html

/src/method_reference/Person.java

package method_reference;

import java.time.LocalDate;

public class Person {
    public Person(String name, LocalDate birthday){
        this.name = name;
        this.birthday = birthday;
    }

    String name;
    LocalDate birthday;

    public LocalDate getBirthday(){
        return birthday;
    }

    public static int compareByAge(Person a, Person b){
        return a.birthday.compareTo(b.birthday);
    }

    @Override
    public String toString(){
        return this.name;
    }

}

/src/method_reference/testMethodReference.java

package method_reference;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.Comparator;
import org.junit.Test;
import method_reference.Person;

public class testMethodReference {

    @Test
    public void test(){
        Person[] pArr = new Person[]{
                new Person("003", LocalDate.of(2016,9,1)),
                new Person("001", LocalDate.of(2016,2,1)),
                new Person("002", LocalDate.of(2016,3,1)),
                new Person("004", LocalDate.of(2016,12,1))
        };

//        //使用匿名类
//        Arrays.sort(pArr, new Comparator<Person>() {
//            @Override
//            public int compare(Person o1, Person o2) {
//                return o1.getBirthday().compareTo(o2.getBirthday());
//            }
//        });

//        //使用lambda表达式 未调用已存在的方法
//        Arrays.sort(pArr, (Person o1, Person o2) -> {
//            return o1.getBirthday().compareTo(o2.getBirthday());
//        });

        //使用方法引用,引用的是类的静态方法
        Arrays.sort(pArr, Person::compareByAge);

        System.out.println(Arrays.asList(pArr));
    }
}

Java之泛型 T与T的用法

<T> T表示返回值是一个泛型,传递啥,就返回啥类型的数据,而单独的T就是表示限制你传递的参数类型,这个案例中,通过一个泛型的返回方式,获取每一个集合中的第一个数据, 通过返回值<T> T 和T的两种方法实现


<T> T 用法

这个<T> T 表示的是返回值T是泛型,T是一个占位符,用来告诉编译器,这个东西先给我留着,等我编译的时候,告诉你。

package com.yellowcong.test;import java.util.ArrayList;import java.util.List;import org.apache.poi.ss.formula.functions.T;public class Demo {

    public static void main(String[] args) {

        Demo demo = new Demo();        //获取string类型
        List<String> array = new ArrayList<String>();
        array.add("test");
        array.add("doub");
        String str = demo.getListFisrt(array);
        System.out.println(str);        //获取nums类型
        List<Integer> nums = new ArrayList<Integer>();
        nums.add(12);
        nums.add(13);

        Integer num = demo.getListFisrt(nums);
        System.out.println(num);
    }    /**
     * 这个<T> T 可以传入任何类型的List
     * 参数T
     *     第一个 表示是泛型
     *     第二个 表示返回的是T类型的数据
     *     第三个 限制参数类型为T
     * @param data
     * @return
     */
    private <T> T getListFisrt(List<T> data) {        if (data == null || data.size() == 0) {            return null;
        }        return data.get(0);
    }

}

T 用法

返回值,直接写T表示限制参数的类型,这种方法一般多用于共同操作一个类对象,然后获取里面的集合信息啥的。

package com.yellowcong.test;import java.util.ArrayList;import java.util.List;

public class Demo2<T> {

    public static void main(String[] args) {        //限制T 为String 类型
        Demo2<String> demo = new Demo2<String>();        //获取string类型
        List<String> array = new ArrayList<String>();
        array.add("test");
        array.add("doub");        String str = demo.getListFisrt(array);        System.out.println(str);        //获取Integer类型 T 为Integer类型
        Demo2<Integer> demo2 = new Demo2<Integer>();        List<Integer> nums = new ArrayList<Integer>();
        nums.add(12);
        nums.add(13);        Integer num = demo2.getListFisrt(nums);        System.out.println(num);
    }    /**
     * 这个只能传递T类型的数据
     * 返回值 就是Demo<T> 实例化传递的对象类型
     * @param data
     * @return
     */
    private T getListFisrt(List<T> data) {        if (data == null || data.size() == 0) {            return null;
        }        return data.get(0);
    }
}

java 泛型详解

对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。

本文参考java 泛型详解、Java中的泛型方法、 java泛型详解

1. 概述

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

什么是泛型?为什么要使用泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

2. 一个栗子

一个被举了无数次的例子:

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

毫无疑问,程序的运行结果会以崩溃结束:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,在使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。

我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。

List<String> arrayList = new ArrayList<String>();
...//arrayList.add(100); 在编译阶段,编译器就会报错

3. 特性

泛型只在编译阶段有效。看下面的代码:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d("泛型测试","类型相同");
}

输出结果:泛型测试: 类型相同

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

4. 泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

1)泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{  
    private 泛型标识 /*(成员变量类型)*/ var; 
    .....
  }
}

一个最普通的泛型类:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;    
    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.Generic<Integer> genericInteger = new Generic<Integer>(123456);
//传入的实参类型需与泛型的类型参数类型相同,即为String.Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());

09-16 09:20:04.432 13063-13063/ 泛型测试: key is 123456
09-16 09:20:04.432 13063-13063/ 泛型测试: key is key_vlaue

定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

看一个例子:

Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);

Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());

泛型测试: key is 111111
泛型测试: key is 4444
泛型测试: key is 55.55
泛型测试: key is false

 

注意:

  • 泛型的类型参数只能是类类型,不能是简单类型。

  • 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。

  if(ex_num instanceof Generic<Number>){ }

2)泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {    
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class" 
*/
 
 class FruitGenerator<T> implements Generator<T>{
    @Override    
    public T next() {        
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。 
*/
 public class FruitGenerator implements Generator<String> {    
    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override    
    public String next() {
        Random rand = new Random();        
        return fruits[rand.nextInt(3)];
    }
}

3)泛型通配符

我们知道IngeterNumber的一个子类,同时在特性章节中我们也验证过Generic<Ingeter>Generic<Number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实例传入呢?在逻辑上类似于Generic<Number>Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?

为了弄清楚这个问题,我们使用Generic<T>这个泛型类继续看下面的例子:

public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

 

Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gInteger);// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> 
// cannot be applied to Generic<java.lang.Number>// showKeyValue(gInteger);

 

通过提示信息我们可以看到Generic<Integer>不能被看作为`Generic<Number>的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic<Integer>类型的类,这显然与java中的多态理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic<Integer>Generic<Number>父类的引用类型。由此类型通配符应运而生。

我们可以将上面的方法改一下:

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!

此处’?’是类型实参,而不是类型形参 ! 

此处’?’是类型实参,而不是类型形参 !

再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。

可以解决当具体类型不确定的时候,这个通配符就是 ?  ;

当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

泛型方法

在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。

尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。

泛型类,是在实例化类的时候指明泛型的具体类型;

泛型方法,是在调用方法的时候指明泛型的具体类型 。

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 
 */
 public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,IllegalAccessException{
        T instance = tClass.newInstance();        
        return instance;
}

 

Object obj = genericMethod(Class.forName("com.test.test"));

1)泛型方法的基本用法

光看上面的例子有的同学可能依然会非常迷糊,我们再通过一个例子,把泛型方法再总结一下。

public class GenericTest {   
   //这个类是个泛型类,在上面已经介绍过
   public class Generic<T>{     
        private T key;        
        public Generic(T key) {            
            this.key = key;
        }        
        //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。        
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。        
        //所以在这个方法中才可以继续使用 T 这个泛型。
        
        public T getKey(){            
            return key;
        }        
        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
            public E setKey(E key){
                 this.key = keu
            }        
        */
    }    
    
    /** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }     
    */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());        
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();        
        return test;
    }   
     
    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }    
    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?    
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Generic<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }     
    
    /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
    public <T> T showKeyName(Generic<E> container){
        ...
    }  
    */

    /**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
    public void showkey(T genericObj){

    }    
    */

    public static void main(String[] args) {


    }
}

2)类中的泛型方法

当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下

public class GenericFruit {    
    class Fruit{
        @Override        
        public String toString() {            
            return "fruit";
        }
    }    
    
    class Apple extends Fruit{
        @Override        
        public String toString() {            
            return "apple";
        }
    }    
    
    class Person{
        @Override        
        public String toString() {            
            return "Person";
        }
    }    
    
    class GenerateTest<T>{        
        public void show_1(T t){
            System.out.println(t.toString());
        }        
        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。        
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }        
        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }    
    
    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();  
              
        //apple是Fruit的子类,所以这里可以        
        generateTest.show_1(apple);      
          
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person        
        //generateTest.show_1(person);
                
        //使用这两个方法都可以成功        
        generateTest.show_2(apple);
        generateTest.show_2(person); 
               
        //使用这两个方法也都可以成功        
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

3) 泛型方法与可变参数

再看一个泛型方法和可变参数的例子:

public <T> void printMsg( T... args){    
    for(T t : args){
        Log.d("泛型测试","t is " + t);
    }
}
printMsg("111",222,"aaaa","2323.4",55.55);

4) 静态方法与泛型

静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

public class StaticGenerator<T> {
    ....
    ....    
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"     
     */
    public static <T> void show(T t){

    }
}

5) 泛型方法总结

泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:

无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

6) 泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型

public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);
//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

如果我们把泛型类的定义也改一下:

public class Generic<T extends Number>{    
    private T key;    
    public Generic(T key) {        
        this.key = key;
    }    
    public T getKey(){        
        return key;
    }
}

 

//这一行代码也会报错,因为String不是Number的子类Generic<String> generic1 = new Generic<String>("11111");

再来一个泛型方法的例子:

//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();    return test;
}

通过上面的两个例子可以看出:泛型的上下边界添加,必须与泛型的声明在一起 。

7) 关于泛型数组要提一下

看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。

也就是说下面的这个例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10];

这样也是可以的:

List<String>[] ls = new ArrayList[10];

下面使用Sun的一篇文档的一个例子来说明这个问题:

List<String>[] lsa = new List<String>[10]; 
// Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; 
// Unsound, but passes run time store check    
String s = lsa[1].get(0); 
// Run-time error: ClassCastException.

这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。

而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。

下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

List<?>[] lsa = new List<?>[10]; 
// OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; 
// Correct.    
Integer i = (Integer) lsa[1].get(0); 
// OK

5. 最后

本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。

JAVA 子类调用父类方法时,方法中的变量用谁的?

public class T1 {
    private int a=6;
 
    public void ha(){
        System.out.println(this.a);
    }
}
public class T2 extends T1{
        int a=7;
}
public class Test {
    public static void main(String[] args) {
        new T2().ha();
    }
}

输出结果为6

结论:子类调用父类方法时,方法中的变量用父类的

这个结果是很有意思的,我之前以为既然是子类调用父类的方法 那么应该是子类自动继承了父类 然后在自己类的内部使用自己的变量

没想到会使用父类内定义的变量值