Java之泛型相关

1.泛型介绍

泛型(Generic)是在JDK5引入的新特性,提供了编译时类型安全的校验机制,可以在编译期发现类型错误,提高了代码的类型安全性,并且编译期的错误修复比运行期的错误容易的多,给开发也提供了便利;

泛型的本质是参数,可以定义在类,接口和方法中,也就是说指定操作的数据类型为参数类型,和方法的形参有些类似,不同的是形参传入的是具体值,而泛型传入的是一个类型

泛型必须是包装类型,不能是int,double,float这种基本类型

泛型按照使用位置划分可以分为三类
1.定义在类上

public class Test<T>{
        private T data;

        public T getData() {
            return data;
        }

        public void setData(T data) {
            this.data = data;
        }
}

2.定义在接口上

public interface Test<T> {
        void test(T data);
}

3.定义在方法上

 public <T> void test(T data){
        //todo
 }

2.泛型的类型检查

使用泛型可以避免编译的类型检查

当我们指定使用一个确定的泛型时,如果我们传入非指定的参数或其的派生子类,那么编译期会帮我们指出这个类型错误问题,直接在编译时抛出这个异常

List list = new ArrayList();
list.add("test");
list.add(123);		//编译通过	
list.add(true);		//编译通过
 String result = (String) list.get(0);

List<String> list2 = new ArrayList<>();
list2.add("test2");
list2.add(1);		//编译报红
list2.add(true);	//编译报红
 String result2 = list2.get(0);

当未指定类型时,则会默认为Object的泛型类型,同时可以看出,获取和泛型参数相同的属性时候无需类型检查并强转

然而,虚拟机中并没有泛型这个概念,所有的入参都是Object类型,当你定义一个泛型类,然后定义几个重载方法

    public class Test<T> {
        T data;
        public void setData(float test){}
        public void setData(int test){}
        public void setData(T data){}          //和Object冲突
        public void setData(Object object){}   //和T泛型冲突
  
  	    public T getData() { return data; }
    }

会发现,泛型的重载方法和Object的重载方法冲突,也就是说T泛型的重载实质上就是制定的Object入参

可能不太直观,我们在Studio装一个AMS Bytecode Outline插件,然后把上面的object的重载方法删除,然后给这个类一个泛型并调用设置和取出的方法,编译时可以看出这里并没有强转

Test<String> test = new Test<>();
test.setData("2333");
String result = test.getData();

然后我们再右键选择show bytecode,查看具体的字节码代码

  _new 'com/example/java/fanxing/FanxingTest$Test'
    dup
    aload 0
    INVOKESPECIAL com/example/java/fanxing/FanxingTest$Test.<init> (Lcom/example/java/fanxing/FanxingTest;)V
    astore 1
    aload 1
    ldc "2333"
    INVOKEVIRTUAL com/example/java/fanxing/FanxingTest$Test.setData (Ljava/lang/Object;)V
    aload 1
    INVOKEVIRTUAL com/example/java/fanxing/FanxingTest$Test.getData ()Ljava/lang/Object;
    checkcast 'java/lang/String'
    astore 2
    return

或者也可以直接编译查看Test这个类的方法,一样的结果

可以看出即使你定义了类型,但是字节码文件中并没有类型这个属性,设置入参都会以Object方式传递和存储,然后获取的时候会进行一次强制转换,返回转换后的数据,这也就解释了为什么上面会和定义的Object的重载方法冲突,因为这两个其实是同一个方法

3.泛型的类型推断

1.实例化类型推断
如果你如果声明时候指定了泛型类型,那么后续的创建则不需要额外再次指出

Test<String> test1 = new Test<String>();
Test<String> test2 = new Test<>();

2.泛型方法的类型推断
如果你声明了入参为泛型的方法,那么在调用这个方法的时候,会根据传入的参数去推断泛型的类型

public <T> void testList(List<T> list) {
   //dosomething
}
public <T> T testData(T data) {
   //dosomething
   return data;
}
public void test(){
	List<String> list = new ArrayList<>();
    testList(list);

    String data = testData("1234");
}

单独传一个泛型参数的一般用于创建一个实例对象

public <T> T getClassInstance(T pass){
        try {
            return (T) pass.getClass().getConstructor().newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

传泛型参数的方法可以用于做强转换操作,比如下面的例子

 public class Main {
        public HashMap<String, Test> map = new HashMap<>();
        public <T> Test<T> getTestData(String key, Class<T> type) {
            if (map.containsKey(key)) {
                return (Test<T>) map.get(key);
            }
            return null;
        }
    }

值得一提的是,平时我们传入的对象类 Test.class,就等同于这里的 Class< Test >

4.泛型的上下边界

泛型中功能最为强大的就是通配符和边界限制了,根据类型可以分为上界和下界

4.1 通配符

java中默认使用?表示无界的泛型类型,即该类型是未知,而?默认的实现可以认为是? extends Object,表示可以是继承自Object的任意类型,比如上面的例子中的List,如果不指定泛型类型,那么可以认为会用默认的无界通配符,而区别是List不会做泛型检查,List<?>会做相应的泛型检查

泛型通配符只能用于方法,而不能用于类

public class Test<? extends Object>{ } //编译器会报红

这里还得提一下PECS原则,即"Producter Extends,Consumer Super"
如果是从一个泛型数据中取数据,那么就可以使用 ? extends xx,即生产者模式
如果是往一个泛型数据中存数据,那么就可以使用? super xx</font>,即消费者模式
简要概括就是上界不存,下界不取

4.2 上边界 extends

? extends A 泛型通配表示具体的参数类型可以是A以及A的子类
上界也同样适用于普通泛型 即 T extends A也是适用的

class A {}
class B extends A {}
class C extends B {}  
class D {}
public class Test<T> {
  T data;
  public Test(){}
  public Test(T data){
     this.data = data;
  }
  public void setData(T data) {
      this.data = data;
  }
  public T getData() {
      return data;
  }
}

private void test1() {
   Test<? extends B> b = new Test<>();
//   b.setData(new C()); //这里无法用setData方法设置值,编译报红
   invoke(b);
   C data = (C) b.getData();

   Test<? extends B> b1 = new Test<>(new B());
   Test<? extends B> b2 = new Test<>(new C());

   B data1 = b1.getData();
   C data2 = (C) b2.getData();

   System.out.println("data -->>> " + data);
   System.out.println("data1 -->>> " + data1);
   System.out.println("data2 -->>> " + data2)
 }
 
 public void invoke(Test var0) {
    try {
       Method method = var0.getClass().getDeclaredMethod("setData", Object.class);
       method.invoke(var0, new C());
     } catch (Exception e) {
       e.printStackTrace();
     }
 }   

输出结果是

data -->>> com.example.java.fanxing.FanxingTest$C@816f27d
data1-->>> com.example.java.fanxing.FanxingTest$B@3e3abc88
data2 -->>> com.example.java.fanxing.FanxingTest$C@87aac27

可以看出,当用? extends B 指定通配类型时,使用setData方法设置值会在编译期告警,即编译器不允许这种设置方式,但构造方法传值仍然有效;
而获取值的时候只有获取泛型的父类B不用强转,也就是说,extends方法会默认把类型设置为上界的父类,获取到的值都是当前类型的父类信息,而子类转父类是安全的,所以这个一般用于获取值

另外,通过反射方法仍然是可以设置属性的

4.3 泛型的多继承

泛型的继承使用的是extends,可以分为单继承和多继承
多继承指的是类继承和接口实现,同时只能最多继承一个类,接口可以继承多个,当同时有类和接口时,那么类必须放到第一位

 public class A {}
 public interface B {}
 public interface C {}
 public class Test<T extends A>{}
 public class Test<T extends A & B & C> {}
 //public class Test<T extends  B & C&A> {} 这个不正确,类必须放到首位

4.4 下边界 super

? super A 泛型通配表示具体的参数类型可以是A以及A超类
普通泛型T并没有这种表示方法 ,即 T super A是不存在的

private void test1() {
        Test<? super B> b1 = new Test<>(new A());
        A data = (A) b1.getData();
//        b1.setData(new A()); 这里无法添加B的父类A,会报红

        Test<? super B> b2 = new Test<>();
        b2.setData(new B());
        B data2 = (B) b2.getData();

        Test<? super B> b3 = new Test<>();
        b3.setData(new C());
        C data3 = (C) b3.getData();
        

        System.out.println("data1 -->>> " + data);
        System.out.println("data2 -->>> " + data2);
        System.out.println("data3 -->>> " + data3);
    }

super通配指的是只有该类型以及该类型的超类可以正确匹配,而实际的情况可能有些出入
当使用super通配时候,可以通过方法传参的方式正常匹配,比如上面传入了B的超类A,但无法通过设置的方式给一个泛型类设值;也就是说我定义了一个?super B的泛型类,设置值的时候只能添加B以及B的子类,而不能添加B的超类,所以super对于独立的泛型类并不适合,更多场景是用于构造方法或者泛型方法内的适用
同时也可以看到上面同样获取值也是可以的,但是所有获取的值都需要进行强制转换,也就是说编译器并不知道存的是什么样的值,因为一个类的超类转成子类是不安全的,比如一个Object类转成Interger类型,除非已经确定是这个类型,否则会出转换异常,所以这个super通常也只用于存信息
刚提到super一般常用于泛型方法中,比如Collections的一个copy方法

 public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

这里的dest只负责存,而src只负责取
这里有个设置的方法

 dest.set(i, src.get(i));

dest里面设置是从src中获取的,而src的泛型类型是extedns,也就是说dest中设置的值都是dest的泛型的子类,是和我们上面提到的只能添加本身和子类一致
这里比如我们假定泛型是Integer,那么dest可以是Number , src暂时就指定本身,就有

 List<Number> list = new ArrayList<>();
 List<Integer> list1 = new ArrayList<>();
 Collections.copy(list,list1);

这个是成立的

所以虽然定义了?super T 的这种格式,理论上我们可以使用相应的超类信息,但实际上这个一般只界定了这个下界,就是这个至少是个什么东西,存的时候一般也只会存这个下界信息,就是上面的List<Number>里会去存List<Integer>内的值,而不是会去存超类Object

5. 泛型擦除

上面提到过,JVM是不会识别到泛型信息的,在编译的字节码文件中,编译器会把泛型中的所有参数替换为相应的边界类型,如果未指定则会替换为Object类型,因此编译出的字节码文件只会包含相应的类信息,接口,方法等,也就是说运行期是不会出现泛型的

另外,所有的泛型信息在字节码中都会编译成Object类型,并最终进行相应的强制转换成为我们所需要的类型,所以重载方法需要注意不要和Object参数的方法的冲突,即定义和Object入参的相同方法名的泛型方法

5.1 泛型的桥方法

泛型可以定义继承类或接口,可以有很好的拓展性,并保持类的多态性

比如有一个场景,我们要比较两个泛型的值,找到匹配的哪一个,但泛型的具体表现是未知的,一般我们需要这样写

 public <T> boolean compare(T[] dest, T src) {
        for (T item : dest) {
            //dosomething
            return true;
        }
        return false;
    }

然后会发现无法进行处理,强转不显示,这里就需要泛型也能用相应的比较方法才可以
然后我们把方法进行修改,拓展泛型的实现

public <T extends Bridge<T>> boolean compare(T[] dest, T src) {
        for (T item : dest) {
            if (item.match(src)) return true;
        }
        return false;
    }
    
    public interface Bridge<T> {
        boolean match(T target);
    }

这里用一个接口来实现,泛型实现了这个接口,但泛型参数保持不变,也就是说泛型具备了这个接口的功能和方法,当然传入的参数也必须是要是这个接口的实现类才可以

public class Test implements Bridge<Test> {
        private String data;
        public Test(String data) {
            this.data = data;
        }
        @Override
        public boolean match(Test target) {
            return data.equals(target.data);
        }
    }  
 public void test() {
        Test data[] = new Test[3];
        data[0] = new Test("1");
        data[1] = new Test("2");
        data[2] = new Test("3");
        System.out.println(compare(data, new Test("4")));
        System.out.println(compare(data, new Test("1")));
        System.out.println(compare(data, new Test("3")));
//    String[] data2 =new String[2];
//    data2[0] ="4";
//    data2[1] = "5";
//    System.out.println(compare(data2, "4")); //泛型不匹配,报红
    }

之前说过,所有泛型在编译成字节码后都会擦除掉相应的泛型类型,统一会用Object进行替换,并在合适的时间进行转换,这里的接口实现方法也是如此,查看字节码我们会发现我的接口实现会有两个同名的有match方法

 @groovyx.ast.bytecode.Bytecode
  public boolean match(com.example.java.fanxing.FanxingTest3$Test a) {
    aload 0
    getfield 'com/example/java/fanxing/FanxingTest3$Test.data','Ljava/lang/String;'
    aload 1
    getfield 'com/example/java/fanxing/FanxingTest3$Test.data','Ljava/lang/String;'
    INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
    ireturn
  }

  @groovyx.ast.bytecode.Bytecode
  public volatile boolean match(Object a) {
    aload 0
    aload 1
    checkcast 'com/example/java/fanxing/FanxingTest3$Test'
    INVOKEVIRTUAL com/example/java/fanxing/FanxingTest3$Test.match (Lcom/example/java/fanxing/FanxingTest3$Test;)Z
    ireturn
  }

一个是我们本身实现的方法,而另一个是泛型的默认实现,也就是上面提的泛型会转成Object,然后这个泛型的Object方法会把里面的Object值进行强转并指向我们自己实现的match的方法
同样的,在这个Test类中再定义一个同名的方法,入参设置为Object也会报错的,因为和泛型默认实现的相冲突

那么我们就得注意一下,定义泛型入参方法避免使用Object的相关同名方法,比如equal,因为泛型入参会默认编译成Object,如果有其他同名的Object参数方法会提示冲突

这里的这个泛型方法强转并重新指向的我们称为桥方法,这是编译器在编译一个扩展参数化或实现参数化接口的类时,创建的一个合成方法

6.泛型的联系

6.1 泛型的继承关系

比如有两个泛型List<Number>List<Integer>,那么List<Integer>List<Number>的子类吗?
答案是否定的,这两个泛型间没有任何继承关系,但这两个都继承自Object这个超类
同泛型的类之间存在继承关系,比如
ArrayList<String>继承List<String>,而后者继承Collection<String>
多泛型参数,主泛型匹配的也存在继承关系,比如两个类

public class A<T> {}
public class B<T, V> extends A<T> {}

那么就有B<String,Integer>继承A<String>,同时B<String,Number>也是继承A<String>的,但这两个B之间没有任何继承关系
但通配符则使得泛型间的继承关系成为可能,比如有IntegerNumber两种类型,以及相应的上下界通配,那么继承关系会有如下几种情况,以->表示继承方向
1、 List<Integer> -> List<? extends Integer> -> List<? extends Number> -> List<?>
2. List<Number> -> List<? super Number> -> List<? super Integer> -> List<?>
3. List<Integer> -> List<? super Integer> -> List<?>
4. List<Number> -> List<? extends Number> -> List<?>
或者说这里的子类的值可以直接赋值给父类而不用强转

6.2 协变

? extends T按照定义是无法往里面添加元素的,但如果我们知道某类型是该泛型的子类实现,然后想添加进去该怎么处理,这里就需要用到泛型的协变
? extends T实现了协变的功能

 public class Parent {}
 public class Child extends Parent {}
 private void test() {
        List<? extends Parent> parentList = new ArrayList<>();
//        parentList.add(new Child());
//        parentList.add(new Child()); //无法添加
        List<Child> childList = new ArrayList<>();
        childList.add(new Child());
        childList.add(new Child());
        
        parentList = childList;
        
        for (Parent item : parentList) {
            System.out.println(item);
        }
    }

这里指定了一个泛型List<? extends Parent> ,同时也创建了一个该泛型类型的子类的List即List<Child>,那么这里的Child对于? extends Parent是满足的,即子类可以安全的转型为父类,符合里式替换原则,那么这里的集合parentList可以赋值为childlList而不报错,这就是所谓的协变

这里的理解是ParentChild的父类,同时List<? extends Parent>List<Child>的父类,这种关系是协变

java中数组是自带协变的

6.3 逆变

? super T则实现了逆变的功能

public class Parent {}
public class Child extends Parent {}
public class Baby extends Child {}
private void test() {
   List<? super Child> childList = new ArrayList<>();
   List<Parent> parentList = new ArrayList<>();
   parentList.add(new Child());
   parentList.add(new Parent());
   parentList.add(new Baby());

   childList = parentList;
   for (int i = 0; i < childList.size(); i++) {
       System.out.println(childList.get(i));
   }
}

这里的理解是ParentChild的父类,而List<Parent>List<? super Child>的子类,这种逆关系称之为逆变

7.泛型的限制

1.原始类型,如int,double,float无法作为泛型的参数,必须使用他们的包装类Integer等
2.泛型的参数无法创建实例,即我们未指定泛型类型,方法中的泛型参数是无法实例化的,这种一般可以通过反射就构建
3.静态变量无法定义为泛型,因为一个静态变量内存中只存在一份,不可能指定多个类型
4.无法捕获或抛出泛型化对象,即自定义泛型异常无法抛出
5.泛型化参数无法使用匹配关键字,即 instance of

8.堆污染

当一个参数化类型变量引用了一个对象,而这个对象并非此变量的参数化类型时,这时就会发生堆污染
比如下面的例子

 private void test2() {
        Test test1 = new Test<String>();
        Test<Integer> test2 = test1;
    }

先声明一个无界的泛型类型然后指向一个String泛型类型的实例,然后再把这个实例指向一个Integer类型的对象,这里就会发生堆污染
因为泛型擦除后,这两个对象都会丢失泛型信息,同时第二个泛型的实例指向第一个泛型,那么这里一个类便拥有两种泛型信息,这显示是不对的,编译器也就无法确定具体是哪种泛型类型

同样的,当我们的方法指定了可变参数时,也有发生堆污染的可能

比如可变泛型参数T...T[] ,泛型擦除后根,会变成Object[]的结构,这样就可能造成潜在的堆污染

避免堆污染警告
@SafeVarargs:当你确定操作不会带来堆污染时,使用此注释关闭警告
@SuppressWarnings({"unchecked", "varargs"}):强制关闭警告弹出(不建议这么做)

9.思考

以下答案部分摘自网络
1.泛型是什么,泛型的好处是什么?
泛型时一种参数化类型的机制,可以增强代码的健壮性,方便进行一系列拓展操作;泛型时一种编译时类型的确认机制,提供了编译期的类型安全,即定义了泛型类型的只能使用正确匹配的该类型的对象,避免转换异常;
泛型的类型检查提前到编译期,便于更早发现错误

2.List< Object >和List有什么区别?
原始类型和带参数类型< Object >之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。它们之间的第二点区别是,你可以把任何带参数的泛型类型传递给接受原始类型List的方法,但却不能把List< String >传递给接受List< Object >的方法,因为会产生编译错误。
3.Java中List<?>和List< Object>之间的区别是什么?
实质上却完全不同。List<?> 是一个未知类型的List,而List< Object>其实是任意类型的List。你可以把List< String>, List< Integer>赋值给List<?>,却不能把List< String>赋值给List< Object>
而未知的当然也不能添加值,null除外

List<?> list = new ArrayList<Integer>();
//list.add(1);      报红
//list.add(true);   报红
list.add(null);

List<Object> list2 = new ArrayList<>();
list2.add(1);
list2.add(true);
list.add(null);

4.泛型是如何工作的?什么是泛型的擦除
泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。
编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List< String >在运行时仅用一个List类型来表示。为什么要进行擦除呢?这是为了避免类型膨胀。伪泛型,虚拟机不支持
5.C++模板和java泛型之间有何不同?
java泛型实现根植于“类型消除”这一概念。当源代码被转换为Java虚拟机字节码时,这种技术会消除参数化类型。有了Java泛型,我们可以做的事情也并没有真正改变多少;他只是让代码变得漂亮些。鉴于此,Java泛型有时也被称为“语法糖”。

这和 C++模板截然不同。在 C++中,模板本质上就是一套宏指令集,只是换了个名头,编译器会针对每种类型创建一份模板代码的副本。

由于架构设计上的差异,Java泛型和C++模板有很多不同点:

C++模板可以使用int等基本数据类型。Java则不行,必须转而使用Integer。

在Java中,可以将模板的参数类型限定为某种特定类型。

在C++中,类型参数可以实例化,但java不支持。

在Java中,类型参数不能用于静态方法(?)和变量,因为它们会被不同类型参数指定的实例共享。在C++,这些类时不同的,因此类型参数可以用于静态方法和静态变量。

在Java中,不管类型参数是什么,所有的实例变量都是同一类型。类型参数会在运行时被抹去。在C++中,类型参数不同,实例变量也不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值