java基础-泛型

2. java 泛型

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?

顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,

操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

2.1 什么是泛型

自JDK1.5之后,java通过泛型解决了容器类型安全这一问题。
  • 泛型的本质是参数化类型

    也就是说,泛型就是将所操作的数据类型作为参数的一种语法。
    
  • 泛型的作用

    • 使用泛型能写出更加通用灵活的代码

    • 泛型将代码安全性检查提前到编译期

      泛型被加入Java语法中,还有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时比如:
      
      List dogs =new ArrayList();
      dogs.add(new Cat());
      

      在没有泛型之前,这种代码除非运行,否则你永远找不到它的错误。但是加入泛型后,会在编译的时候就检查出来。

      List<Dog> dogs=new ArrayList<>();
      dogs.add(new Cat());//Error Compile
      
    • 泛型能够省去类型强制转换

      在JDK1.5之前,java容器都是通过将类型向上转型为object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。

      Dog dog=(Dog)dogs.get(1);
      

​ 加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行类型转换,使得代码更加优雅。

2.2 泛型的具体实现

我们可以定义泛型类泛型方法泛型接口等,那泛型的底层是怎么实现的呢?

  • 泛型的擦除

    Java 设计者将泛型完全作为了语法糖加入了新的语法中,什么意思呢?也就是说泛型对于JVM来说是透明的,有泛型的和没有泛型的代码,通过编译器编译后所生成的二进制代码是完全相同的。这个语法糖的实现被称为擦除
    
  • 泛型擦除的过程

    泛型是编译时将具体的类型作为类型参数传递给方法、类、接口。

    擦除是在代码运行过程中将具体的类型都抹除。

    JDK1.5之前需要编写模板代码的地方都是通过object来保存具体的值。比如:

    public class Node{
       private Object obj;
    
       public Object get(){
           return obj;
       }
       
       public void set(Object obj){
           this.obj=obj;
       }
       
       public static void main(String[] argv){
        
        Student stu=new Student();
        Node  node=new Node();
        node.set(stu);
        Student stu2=(Student)node.get();
       }
    }
    

    这样的实现能满足绝大多数需求,但是泛型还是有更多方便的地方,最大的一点就是编译期类型检查,于是Java 1.5之后加入了泛型,但是这个泛型仅仅是在编译的时候帮你做了编译时类型的检查,成功编译后生成的.class文件还是一模一样的,这便是擦除。

    JDK1.5以后的实现

    public class Node<T>{
    
        private T obj;
        
        public T get(){
            
            return obj;
        }
        
        public void set(T obj){
            this.obj=obj;
        }
        
        public static void main(String[] argv){
        
        Student stu=new Student();
        Node<Student>  node=new Node<>();
        node.set(stu);
        Student stu2=node.get();
      }
    }
    

    两个版本生成的.class文件

    Node:

      public Node();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
      public java.lang.Object get();
        Code:
           0: aload_0
           1: getfield      #2                  // Field obj:Ljava/lang/Object;
           4: areturn
      public void set(java.lang.Object);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #2                  // Field obj:Ljava/lang/Object;
           5: return
    }
    
    public class Node<T> {
      public Node();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
      public T get();
        Code:
           0: aload_0
           1: getfield      #2                  // Field obj:Ljava/lang/Object;
           4: areturn
    
      public void set(T);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #2                  // Field obj:Ljava/lang/Object;
           5: return
    }
    

    可以看到泛型就是使用泛型代码的时候,将类型信息传递给具体的泛型代码,而经过编译后,生成的.class文件和原始的代码一模一样,就好像传递过来的类型信息又被擦除了一样。

2.3 泛型语法

Java 的泛型就是一个语法糖,而语法糖最大的好处就是让人方便使用,但是它的缺点也在于如果不剥开这颗语法糖,有很多奇怪的语法就很难理解。

2.3.1 类型边界

泛型最终会擦除为object类型。这样导致的是在编写泛型代码的时候,对泛型元素的操作只能使用object自带的一些方法,但是有时候我们想使用其它类型的方法怎么办?

比如:

public class Node{
    private People obj;
    public People get(){
        
        return obj;
    }
    
    public void set(People obj){
        this.obj=obj;
    }
    
    public void playName(){
        System.out.println(obj.getName());
    }
}

代码中需要使用obj.getName()方法,因此比如规定传入的元素必须是People及其子类,那么这样的方法怎么通过泛型体现出来呢?

答案是:extends,泛型重载了extends关键字,可以通过extends关键字指定最终擦除所替代的类型。

public class Node<T extends People>{

    private T obj;
    
    public T get(){
        
        return obj;
    }
    
    public void set(T obj){
        this.obj=obj;
    }
    
    public void playName(){
        System.out.println(obj.getName());
    }
}

通过extend关键字,编译器会将最后类型都擦除为People类型,就好像最开始我们看见的原始代码一样。

2.3.2 泛型与向上转型的概念

先讲一讲几个概念:

  • 协变:子类能向父类转换Animal a = new Cat();
  • 逆变:父类能向子类转换Cat c = (Cat)a;
  • 不变:两者均不能转换

对于协变,我们见得最多的就是多态,而逆变常见于强制类型转换。

public static void error(){
   Object[] nums=new Integer[3];
   nums[0]=3.2;
   nums[1]="string"; //运行时报错,nums运行时类型是Integer[]
   nums[2]='2';
 }

因为数组是协变的,因此Integer[]可以转换为Object[],在编译阶段编译器只知道numsObject[]类型,而运行时nums则为Integer[]类型,因此上述代码能够编译,但是运行会报错。

这就是常见的人们所说的数组是协变的。这里带来一个问题,为什么数组要设计为协变的呢?既然不让运行,那么通过编译有什么用?

答案是在泛型还没出现之前,数组协变能够解决一些通用的问题:

public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }
/**
 * 摘自JDK 1.8 Arrays.equals()
 */
  public static boolean equals(Object[] a, Object[] a2) {
        //...
        for (int i=0; i<length; i++) {
            Object o1 = a[i];
            Object o2 = a2[i];
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        //..
        return true;
    }

可以看到,只操作数组本身,而不关心数组中具体保存的元素,或者是不管什么元素,取出来就作为一个Object存储的时候,只用编写一个Object[]就能写出通用的数组参数方法。比如:

Arrays.sort(new Student[]{...})
Arrays.sort(new Apple[]{...})

等,但是这样的设计留下来的诟病就是偶尔会出现对数组元素有具体的操作的代码,比如上面的error()方法。

泛型的出现,是为了保证类型安全的问题,如果将泛型也设计为协变的话,那也就违背了泛型最初设计的初衷,因此在Java中,泛型是不变的,什么意思呢?

List<Number>List<Integer> 是没有任何关系的,即使IntegerNumber的子类

也就是对于public static void test(List<Number> nums){...}方法,是无法传递一个List<Integer>参数的。

逆变 一般常见于强制类型转换:

Object obj="test";
String str=(String)obj;

原理便是Java 反射机制能够记住变量obj的实际类型,在强制类型转换的时候发现obj实际上是一个String类型,于是就正常的通过了运行。

2.3.3 泛型与向上转型的实现

前面说了这么多,应该关心的问题在于,如何解决既能使用数组协变带来的方便性,又能得到泛型不变带来的类型安全?

答案依然是extends,super关键字与通配符 ?

泛型重载了extends,super关键字来解决通用泛型的表示。

注意:这句话可能比较熟悉,没错,前面说过extends还被用来指定擦除到的具体类型,比如<E extends Fruit>,表示在运行时将E替换为Fruit,注意E表示的是一个具体的类型,但是这里的extend和通配符连续使用<? extends Fruit>这里通配符?表示一个通用类型,它所表示的泛型在编译的时候,被指定的具体的类型必须是Fruit的子类。比如List<? extends Fruit> list= new ArrayList<Apple>,ArrayList<>中指定的类型可以是Apple,Orange等。不要混淆。
  • 协变泛型(上界): < ? extends ***>
public static  void playFruit(List < ? extends Fruit> list){
    //do somthing
}

public static void main(String[] args) {
    List<Apple> apples=new ArrayList<>();
    List<Orange> oranges=new ArrayList<>();
    List<Food> foods =new ArrayList<>();
    playFruit(apples);
    playFruit(oranges);
    //playFruit(foods); 编译错误
}

可以看到,参数List < ? extends Fruit>所表示是需要一个List<>,其中尖括号所指定的具体类型必须是继承自Fruit的。

public static  void main(String[] args) throws Exception {
		List<? extends Fruit> top = new ArrayList<>();
		top.add(null);
		//top.add(new Apple());//编译报错
		Fruit fruit = top.get(0);
}

上界<? extend Fruit> ,表示所有继承Fruit的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Fruit,所以,我都可以用最大的父类Fruit接着,也就是把所有的子类向上转型为Fruit。

  • 逆变泛型(下界):<? super ***>

    public  static  void playFruitBase(List < ? super  Fruit> list){
         //..
    }
    
    public static void main(String[] args) {
        List<Apple> apples=new ArrayList<>();
        List<Food> foods =new ArrayList<>();
        List<Object> objects=new ArrayList<>();
        playFruitBase(foods);
        playFruitBase(objects);
        //playFruitBase(apples); 编译错误
    }
    

    同理,参数List < ? super Fruit>所表示是需要一个List<>,其中尖括号所指定的具体类型必须是Fruit的父类类型。

    	public static  void main(String[] args) throws Exception {
    		List<? super Apple> bottom = new ArrayList<>();
    		bottom.add(new Apple());
    		//Apple apple = bottom.get(0);//编译不通过
    		Apple object = (Apple) bottom.get(0);
    	}
    

    下界<? super Apple>,表示Apple的所有父类,包括Fruit,一直可以追溯到老祖宗Object 。那么当我add的时候,我不能add Apple的父类,因为不能确定List里面存放的到底是哪个父类。但是我可以add Apple及其子类。因为不管我的子类是什么类型,它都可以向上转型为Apple及其所有的父类甚至转型为Object 。但是当我get的时候,Apple的父类这么多,我用什么接着呢,除了Object,其他的都接不住。

    所以,归根结底可以用一句话表示,那就是编译器可以支持向上转型,但不支持向下转型。具体来讲,我可以把Apple对象赋值给Fruit的引用,但是如果把Fruit对象赋值给Apple的引用就必须得用cast。

  • 无界通用符: ?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java泛型Java 5引入的新特性,可以提高代码的可读性和安全性,降低代码的耦合度。泛型是将类型参数化,实现代码的通用性。 一、泛型的基本语法 在声明类、接口、方法时可以使用泛型泛型的声明方式为在类名、接口名、方法名后面加上尖括号<>,括号中可以声明一个或多个类型参数,多个类型参数之间用逗号隔开。例如: ```java public class GenericClass<T> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T> { T getData(); void setData(T data); } public <T> void genericMethod(T data) { System.out.println(data); } ``` 其中,`GenericClass`是一个泛型类,`GenericInterface`是一个泛型接口,`genericMethod`是一个泛型方法。在这些声明中,`<T>`就是类型参数,可以用任何字母代替。 二、泛型使用 1. 泛型类的使用使用泛型类时,需要在类名后面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java GenericClass<String> gc = new GenericClass<>(); gc.setData("Hello World"); String data = gc.getData(); ``` 在这个例子中,`GenericClass`被声明为一个泛型类,`<String>`指定了具体的类型参数,即`data`字段的类型为`String`,`gc`对象被创建时没有指定类型参数,因为编译器可以根据上下文自动推断出类型参数为`String`。 2. 泛型接口的使用使用泛型接口时,也需要在接口名后面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java GenericInterface<String> gi = new GenericInterface<String>() { private String data; @Override public String getData() { return data; } @Override public void setData(String data) { this.data = data; } }; gi.setData("Hello World"); String data = gi.getData(); ``` 在这个例子中,`GenericInterface`被声明为一个泛型接口,`<String>`指定了具体的类型参数,匿名内部类实现了该接口,并使用`String`作为类型参数。 3. 泛型方法的使用使用泛型方法时,需要在方法名前面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java genericMethod("Hello World"); ``` 在这个例子中,`genericMethod`被声明为一个泛型方法,`<T>`指定了类型参数,`T data`表示一个类型为`T`的参数,调用时可以传入任何类型的参数。 三、泛型的通配符 有时候,我们不知道泛型的具体类型,可以使用通配符`?`。通配符可以作为类型参数出现在方法的参数类型或返回类型中,但不能用于声明泛型类或泛型接口。例如: ```java public void printList(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } } ``` 在这个例子中,`printList`方法的参数类型为`List<?>`,表示可以接受任何类型的`List`,无论是`List<String>`还是`List<Integer>`都可以。在方法内部,使用`Object`类型来遍历`List`中的元素。 四、泛型的继承 泛型类和泛型接口可以继承或实现其他泛型类或泛型接口,可以使用子类或实现类的类型参数来替换父类或接口的类型参数。例如: ```java public class SubGenericClass<T> extends GenericClass<T> {} public class SubGenericInterface<T> implements GenericInterface<T> { private T data; @Override public T getData() { return data; } @Override public void setData(T data) { this.data = data; } } ``` 在这个例子中,`SubGenericClass`继承了`GenericClass`,并使用了相同的类型参数`T`,`SubGenericInterface`实现了`GenericInterface`,也使用了相同的类型参数`T`。 五、泛型限定 有时候,我们需要对泛型类型参数进行限定,使其只能是某个类或接口的子类或实现类。可以使用`extends`关键字来限定类型参数的上限,或使用`super`关键字来限定类型参数的下限。例如: ```java public class GenericClass<T extends Number> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T extends Comparable<T>> { T getData(); void setData(T data); } ``` 在这个例子中,`GenericClass`的类型参数`T`被限定为`Number`的子类,`GenericInterface`的类型参数`T`被限定为实现了`Comparable`接口的类。 六、泛型的擦除 在Java中,泛型信息只存在于代码编译阶段,在编译后的字节码中会被擦除。在运行时,无法获取泛型的具体类型。例如: ```java public void genericMethod(List<String> list) { System.out.println(list.getClass()); } ``` 在这个例子中,`list`的类型为`List<String>`,但是在运行时,`getClass`返回的类型为`java.util.ArrayList`,因为泛型信息已经被擦除了。 七、泛型类型推断 在Java 7中,引入了钻石操作符<>,可以使用它来省略类型参数的声明。例如: ```java List<String> list = new ArrayList<>(); ``` 在这个例子中,`ArrayList`的类型参数可以被编译器自动推断为`String`。 八、总结 Java泛型是一个强大的特性,可以提高代码的可读性和安全性,降低代码的耦合度。在使用泛型时,需要注意它的基本语法、使用方法、通配符、继承、限定、擦除和类型推断等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值