泛型

1、参数化类型,解决数据类型的安全性问题

2、在声明时,通过一个标识,表示某个属性的类型 / 某个方法返回值的类型 / 参数类型

3、事项

(1)泛型的具体数据类型在定义对象时(编译阶段)指定

(2)泛型只能是引用数据类型

(3)泛型指定具体数据类型后,可以传入该类型或其子类类型(向上转型)

(4)泛型不具备继承性

List<Object> arrayList = new ArrayList<String>();//错误

(5)简写

ArrayList<String> arrayList = new ArrayList<String>();
//等价于
ArrayList<String> arrayList = new ArrayList<>();

(6)默认 Object

ArrayList<Object> arrayList = new ArrayList<>();
//等价于
ArrayList arrayList = new ArrayList<>();

 

自定义泛型

1、自定义泛型类

(1)普通成员可以使用泛型,静态成员不能使用泛型(类加载,对象未创建)

(2)使用泛型的数组,可以声明,但不能初始化(new 时不能确定类型,无法开辟空间)

(3)创建对象时指定泛型的类型,若没有指定泛型,默认为 Object

(4)泛型标识符一般为单个大写字母

2、自定义泛型接口

(1)继承 / 实现接口时确定泛型的类型,若没有指定泛型,默认为 Object

(2)其余规则与自定义泛型类一致

3、自定义泛型方法

(1)定义在泛型类 / 普通类

(2)泛型方法被调用时,泛型的类型被确定

(3)方法可以使用类 / 接口声明的泛型,同时声明泛型

(4)静态方法不能使用类 / 接口声明的泛型,但可以使用自身的泛型

 

通配符

1、<?>:支持任意数据类型的泛型

2、<? extends T>:支持 T 类及 T 类的子类,不限于直接子类,规定泛型上限

(1)由于指定 T 为所有元素的父类,任何时候都可以安全的用 T 来使用容器里的元素

(2)由于以 T 为祖先的子树有很多,不同子树并不兼容,由于实参可能来自于任何一颗子树,所以写入很可能破坏函数实参

(3)这种形式的限定符可以用来读取数据,因为从集合或数组中读取数据出来的数据类型一定是 T 或其子类

3、<? super T>:支持 T 类及 T 类的父类,不限于直接父类,规定泛型下限

(1)? 代表容器里的元素类型,由于只规定了元素必须是 T 的超类,导致元素没有明确统一的父类,除了 Object

(2)这个泛型其实无法使用它,除了把元素强制转成 Object

(3)这种形式的限定符主要用来放入数据,因为可以保证放入 T 或其子类的数据一定满足限定

4、<T>、<?>的区别

(1)<T>:主要用于声明泛型

(2)<?>:主要用于使用泛型

 

T 类型变量、? 通配符

1、使用范围不同

(1)?:参数类型、字段类型、局部变量类型,有时作为返回类型(但请避免这样做)

(2)T:声明类的类型参数、通用方法的类型参数(类型参数、参数类型是两个概念)

2、通常使用 ? 时,并不知道也不关心类型,只想使用其通用的方法,而且 ? 是无法作用于声明类的类型参数,一般作用于方法和参数上;而 T 在类定义时具有更广泛的应用

3、在某些程度的使用上 ? 与 T 是可以等效的,但是 T 参数类型并不支持下界限制,即 T super SomeTing,而通配符支持 ? super SomeThing

4、如果写一个通用方法,且该方法的逻辑不关心类型,那么就用 ? 来进行适配和限制;如果需要作用域类型(在操作通用数组类型时更明显)或声明类的类型参数时,则使用 T 类型变量

5、类型参数 T 定义一种代表作用域类型的变量;通配符 ? 定义了一组可用于泛型类型的允许类型,通配符的意思是“在这里使用任何类型”

 

<? extends T>:上界通配符

1、实例化时的类只能是定义时类本身或其子类,即 T 是实例的上界

import java.util.*;

class Food{
    String name = "Food";
}
class Fruit extends Food{
    String name = "Fruit";
}
class Apple extends Fruit {
    String name = "Apple";
}

// 定义时指定T为Fruit
List<? extends Fruit> list;

// 实例化时类只能是T本身或它的子类
list= new ArrayList<Fruit>(); //可以
list= new ArrayList<Apple>(); //可以
list= new ArrayList<Food>();  //报错

2、上界通配符 add 失效,只能 add null,可以 get

list = new ArrayList<Fruit>();
list.add(new Fruit()); //报错
list.add(null);  //可以
list.get(0); //可以

3、需要在实例化时,就添加元素

list = new ArrayList<Fruit>(){{
    add(new Fruit()); // Fruit及其子类都可以放进去
    add(new Apple()); // Fruit及其子类都可以放进去
}};

4、所添加对象的类型,其上界为实例化时指定的 T,而不是定义时指定的 T 指定

List<? extends Fruit> list= new ArrayList<Apple>(){{
    add(new Fruit()); // 实例化时指定的T为Apple,放入Fruit对象会报错
    add(new Apple()); // Apple及其子类都可以放进去
}};

 

<? super T>:下界通配符

1、实例化时的类只能是定义时类本身或其父类,即 T 是实例的下界

// 定义时指定T为Fruit
List<? super Fruit> list2;

// 实例化时只能是T本身或它的父类
list2 = new ArrayList<Fruit>(); // 可以
list2 = new ArrayList<Food>(); // 可以
list2 = new ArrayList<Apple>();  // 报错

2、在实例化时添加的对象只有一个限制:上界为实例化时指定的 T

class Food{
    String name = "Food";
}
// 新定义一个Meat类,和Fruit都继承Food
class Meat extends Food{
    String name = "Meat";
}
class Fruit extends Food{
    String name = "Fruit";
}
class Apple extends Fruit {
    String name = "Apple";
}

List<? super Fruit> list2 = new ArrayList<Food>(){{
    add(new Object()); // 报错,上界为Food
    add(new Meat()); // 可以,是Food的子类,即使不是Fruit
    add(new Food()); // 可以
    add(new Fruit()); // 可以,是Food的子类
    add(new Apple()); // 可以,是Food的子类
}};

3、在实例化后添加的对象,其上界是定义时指定的 T

list2.add(new Meat());   // 报错,不是Fruit或其子类
list2.add(new Food());   // 报错,不是Fruit或其子类
list2.add(new Fruit());  // 可以
list2.add(new Apple());  // 可以

4、下界通配符 get 的对象默认是 Object 类型,可以做强制类型转换,可以 add

Object object = list2.get(0);
Meat meat = (Meat)list2.get(0); // 如果不是Object,需要强制类型转换

 

无限定通配符

1、<?> 通配符既没有 extends,也没有 super,因此:

(1)不允许调用 set(T) 方法并传入引用(null 除外)

(2)不允许调用 T get() 方法并获取 T 引用(只能获取 Object 引用)

(3)既不能读,也不能写,那只能做一些 null 判断

static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}

(4)大多数情况下,可以引入泛型参数 <T> 消除 <?> 通配符

static <T> boolean isNull(Pair<T> p) {
    return p.getFirst() == null || p.getLast() == null;
}

2、Pair<?> 是所有 Pair<T> 的超类

3、一个无界通配符是一个有用的方法

(1)编写一个可以使用 Object 类中提供的功能实现的方法

(2)当代码使用泛型类中不依赖于类型参数的方法时

4、例如,List.size 或 List.clear。事实上,Class<?> 被如此经常使用,因为 Class<T> 大多数的方法中不依赖于 T

(1)printList 的目标是打印任何类型的列表,但是它无法实现这个目标,它仅打印一个 Object 实例列表,它不能打印 List <Integer>、List <String>、List <Double> 等,因为它们不是 List <Object> 的子类型

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

(2)要写一个通用的 printList 方法,使用 List <?>

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

(3)因为对于任何具体类型 A,List <A> 是 List <?> 的子类型,所以可以使用 printList 打印任何类型的列表

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

(4)List <Object> 和 List <?> 是不一样的,可以将对象或任何对象的子类型插入到 List <Object> 中,但是,只能将 null 插入到 List <?> 中

(5)List 实际上是指持有任意 Object 类型的原生 List,而 List<?> 是指持有某种具体类型的非原生 List

 

通配符捕获和辅助方法

1、通配符捕获:在某些情况下,编译器推断通配符的类型。例如,一个列表可以被定义为 List <?>, 但是当评估一个表达式时,编译器从代码中推断出一个特定的类型

2、该 WildcardError 示例编译时会产生捕获错误

import java.util.List;

public class WildcardError {

    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

(1)编译器将 i 输入参数处理为 Object 类型

(2)当 foo 方法调用 List.set(int,E) 时,编译器无法确认被插入到列表中的对象的类型,并产生错误

(3)当发生这种类型的错误时,通常意味着编译器认为您将错误的类型分配给变量

(4)因为这个原因,泛型被添加到 Java 语言中,在编译时强制执行类型安全检查

3、解决编译器错误

(1)通过编写一个捕获通配符的私有帮助方法来修复它

(2)在这种情况下,可以通过创建专用辅助方法 fooHelper 来解决该问题

public class WildcardFixed {

    void foo(List<?> i) {
        fooHelper(i);
    }

    //创建Helper方法,以便捕获通配符
    //通过类型推断
    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }
}

(3)由于辅助方法,编译器使用推理来确定 T 是调用中的捕获变量 CAP#1。现在编译成功

(4)按照惯例,辅助方法通常被命名为 originalMethodName Helper

 

常见做法

1、E:集合的元素类型

2、K:键

3、V:值

4、T、U、S:任意类型

 

<T extends BoundingType>

1、T 是限定类型的子类型

2、T 和限定类型可以是类,也可以是接口

3、限定类型用 & 分隔,逗号分隔类型变量

(1)根据需要拥有多个接口超类型,但最多一个限定可以是类

(2)如果一个类作为限定,它必须是限定列表中的第一个限定

(3)编译器在必要时向其他限定类型(非第一个)插入强制类型转换,为了提高效率,应该将标签接口(即没有方法的接口)放在限定列表的末尾

 

类型擦除

1、无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)

2、原始类型的名字就是删去类型参数后的泛型类型名

3、擦除(erased)类型变量,并替换为第一个限定类型(无限定的变量则替换为 Object)

 

转换泛型表达式

1、编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

(1)getFirst 擦除类型后的返回值类型是 Object,编译器自动插入到 Employee 的强制类型转换

(2)编译器将对该条泛型方法的调用翻译成两条虚拟机指令

调用原始方法 Pair.getFirst(返回类型为 Object)
将返回的 Object 类型转换为 Employee 类型

2、访问一个范型字段时也要插入强制类型转换

Employee buddy = buddies.first;

 

桥方法

1、转换泛型方法

(1)源代码

public class Pair<T> {
    private T first;
    private T second;
    public Pair(T first; T second) {
        this.first = first;
        this.second = second;
    }
    
    public T getFirst() { return first; }
    public void setFirst(T first) { this.first = first; }
    
    public T getSecond() { return second; }
    public void setSecond(T second) { this.second = second; }
}

class DataInterval extends Pair<LocalDate> {
    // 重写setSecond方法
    public void setSecond(LocalDate second) {
        if (second.compareTo(getFirst) >= 0) {
            super.setSecond(second);
        }
    }
}

/*
    DataInterval 继承 Pair<LocalDate>,此时 Pair<LocalDate> 泛型类型确定
    在类型擦除时,first/second 字段、get/set 方法都会修改为 LocalDate 类型,而 DataInterval 会继承这些方法,私有字段不继承
    现在 DataInterval 实际上有两个 setSecond 方法,一个是 DataInterval 类中重写的 setSecond(LocalDate) 方法,另一个是从父类 Pair<LocalDate> 继承的 setSecond(Object) 方法(类型擦除之后的方法)
    此时类型擦除与多态发生冲突
*/

(2)转换后

/*
    编译器会自动在 DateInterval 类中生成一个桥方法
    程序通过执行 DateInterval.setSecond(Object) 方法,来执行 setSecond(LocalDate) 方法
*/
public void setSecond(Object second) {
    setSecond((LocalDate) second);
}

2、可协变的返回类型

/*
    Employee.clone() 重写 Object.clone()
    实际上,Employee类有两个方法:Employee.clone() 和 Object.clone()
*/
public class Employee implements Cloneable {
    public Employee clone() throws CloneNotSupportedException {
        //省略实现
    }
}

3、 Java 不支持仅有返回值不同的方法重载

(1)但在虚拟机中,会由参数类型和返回值类型共同指定一个方法

(2)因此,编译器可以为两个仅方法返回值不同的方法生成字节码,而虚拟机可以正确的处理这种情况

 

结论

1、虚拟机中没有泛型,只有普通的类和方法

2、所有的类型参数都会替换为它们的限定类型

3、通过合成的桥方法来保证多态

4、为了保证类型安全,必要时会插入强制类型转换

 

限制

1、不能使用基本类型代替类型参数

2、查询运行类型只产生原始类型

3、不能实例化参数化类型的数组,但允许声明参数化类型数组

4、不能实例化类型变量

(1)不能在类似 new T(...) 的表达式中使用类型变量

(2)可以通过 Supplier<T> 提高构造器表达式,或使用反射调用 newInstance

5、不能构造泛型数组

(1)若数组只作为一个类的私有实例字段,可以将该数组的元素类型声明为擦除的类型,并使用强制类型转换

(2)可以通过 Supplier<T> 提高构造器表达式,或使用反射调用 newInstance

6、泛型类的静态上下文中类型变量无效,不能再静态字段或方法中引用类型变量

7、不能抛出或捕获泛型类的实例

(1)泛型类扩展 Throwable 不合法

(2)catch 子句中不能使用类型变量

(3)允许在异常规范中使用类型变量

8、注意擦除后的冲突

(1)重写方法 equals(T value),擦除后会与 Object 类的 equals(Object) 方法冲突,即与合成的桥方法冲突

(2)为了支持擦除转换,要施加一个限制:若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类

 

通配符使用指南

1、将变量看作提供以下两个函数之一是有帮助的

(1)一个 「in」 变量

(2)一个 in 变量将数据提供给代码。想象一下有两个参数的的复制方法 copy(src,dest) 该 src 参数提供的数据被复制,因此它是 in 参数

(3)一个 「out」 变量

(4)out 变量保存用于其他地方的数据。在复制示例 copy(src,dest) 中,dest 参数接受数据,所以它是 out 参数

2、准则

(1)一个 in 变量用一个上界的通配符来定义,使用 extends 关键字

(2)使用 super 关键字定义一个 out 变量,其下界为通配符

(3)在可以使用 Object 类中定义的方法访问 in 变量的情况下,使用无界通配符

(4)在代码需要以 in 和 out 变量访问变量的情况下,不要使用通配符

(5)这些准则不适用于方法的返回类型。应避免使用通配符作为返回类型,因为它强制程序员使用代码来处理通配符

3、由 List<? extends ...> 可以非正式地认为是只读的,但这不是一个严格的保证

(1)假设有以下两个类

class NaturalNumber {

    private int i;

    public NaturalNumber(int i) { this.i = i; }
    // ...
}

class EvenNumber extends NaturalNumber {

    public EvenNumber(int i) { super(i); }
    // ...
}

(2)考虑下面的代码

List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35));  // compile-time error

(3)因为 List <EvenNumber> 是 List<? extends NaturalNumber>,您可以将文件分配给 ln。 但是,您不能使用 ln 将自然数添加到偶数列表中

(4)列表中的以下操作是可能的:可以添加 null、可以调用清除、可以得到迭代器并调用 remove、可以捕获通配符,并写入从列表中读取的元素

posted @   半条咸鱼  阅读(49)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示