Java 基础 一文搞懂泛型

什么是泛型?
泛型的英文是Generics,是指在定义方法、接口或类的时候,不预先指定具体的类型,而使用的时候再指定一个类型的一个特性。

写过Java代码的同学应该知道,我们在定义方法、接口或类的时候,都要指定一个具体的类型。比如:

public class test {
private String name;

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

public String getName() {
    return name;
}

}
上面代码就定义了字段name的类型为String,方法getName的返回类型为String,这种写法就是预先指定了具体的类型。而泛型就是不预先指定具体的类型。

Java中有一个类型叫ArrayList,相当于一个可变长度的数组。在ArrayList类型中就没有预先指定具体的类型。因为数组可以存放任何类型的数据,如果要预先指定一个数组类型的话,那要满足大家对各种类型的需求,就要写很多类型的ArrayList,要为每个class写一个单独的ArrayList,比如:

IntegerArrayList

StringArrayList

FloatArrayList

LongArrayList

这显然不太现实,因为class有上千种,还有自己定义的class。那么在ArrayList中预先指定具体的类型就无法满足需求。这个时候就需要使用泛型,即不指定存储数据的具体的类型,这个类型由使用者决定。

为了解决类型的问题,我们必须把ArrayList变成一种模板:ArrayList,代码如下:

public class ArrayList {
private T[] array;
private int size;
public void add(T e) {…}
public void remove(int index) {…}
public T get(int index) {…}
}
T可以是任何class,这样一来,我们就实现了:编写一次模版,可以创建任意类型的ArrayList:

// 创建可以存储String的ArrayList:
ArrayList strList = new ArrayList();
// 创建可以存储Float的ArrayList:
ArrayList floatList = new ArrayList();
// 创建可以存储Person的ArrayList:
ArrayList personList = new ArrayList();
因此,泛型也可以说是定义一种模板,例如ArrayList,然后在代码中为用到的类创建对应的ArrayList<类型>。(泛型是指在定义方法、接口或类的时候,不预先指定具体的类型,而使用的时候再指定一个类型的一个特性。)后面这种定义可能会更好理解其本质。

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

  1. 为什么要使用泛型?
    参考自:Oracle 泛型文档

与非泛型的代码相比,使用泛型的代码具有很多优点:

在编译时会有更强的类型检查

Java编译器对泛型代码进行强类型检查,如果代码违反类型安全,则会发出错误。修复编译时的错误比修复运行时的错误会更加简单,运行时的错误会更难找到。

说人话就是,使用泛型时,编译器会对输入的类型的进行检查,类型与声明的类型不一致时就会报错。而不使用泛型,编译器可能就检测不到这个类型错误,就会在运行的时候报错。

消除类型转换

下面的代码是没有使用泛型的情况,这时候需要对类型进行转换

List list = new ArrayList();
list.add(“hello”);
String s = (String) list.get(0);
使用泛型,就不需要对类型进行转换

List list = new ArrayList();
list.add(“hello”);
String s = list.get(0); // no cast
可以实现更通用的算法

通过使用泛型,程序员可以对不同类型的集合进行自定义操作以实现通用算法,并且代码类型会更加安全、代码更易读

  1. 如何使用泛型?
    还是以ArrayList为例,如果不定义泛型类型时,泛型类型此时就是Object:

// 编译器警告:
List list = new ArrayList();
list.add(“Hello”);
list.add(“World”);
String first = (String) list.get(0);
String second = (String) list.get(1);
此时,只能把当作Object使用,没有发挥泛型的优势。

当我们定义泛型类型后,List的泛型接口变为强类型List:

// 无编译器警告:
List list = new ArrayList();
list.add(“Hello”);
list.add(“World”);
// 无强制转型:
String first = list.get(0);
String second = list.get(1);
编译器看到泛型类型List就可以自动推断出后面的ArrayList的泛型类型必须是ArrayList,因此,可以把代码简写为:

// 可以省略后面的Number,编译器可以自动推断泛型类型:
List list = new ArrayList<>();
3.1 泛型类
泛型类的语法形式:

class name<T1, T2, …, Tn> { /* … */ }
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。由尖括号(<>)分隔的类型参数部分跟在类名后面。它指定类型参数(也称为类型变量)T1,T2,…和 Tn。

一般将泛型中的类名称为原型,而将 <> 指定的参数称为类型参数。

在泛型出现之前,一个类要想处理所有类型的数据,只能使用Object做数据转换。实例如下:

public class Info {
private Object value;

public Object getValue() {
	return value;
}

public void setValue(Object value) {
	this.value = value;
}

}
使用泛型之后,其实就是将Object换成T,并声明:

public class Info {
private T value;

public T getValue() {
    return value;
}

public void setValue(T value) {
    this.value = value;
}

}
在上面的例子中,在初始化一个泛型类时,使用 <> 指定了内部具体类型,在编译时就会根据这个类型做强类型检查。

实际上,不使用 <> 指定内部具体类型,语法上也是支持的(不推荐这么做),这样的调用就失去泛型类型的优势。如下所示:

public static void main(String[] args) {
Info info = new Info();
info.setValue(10);
System.out.println(info.getValue());
info.setValue(“abc”);
System.out.println(info.getValue());
}
上面是单个类型参数的泛型类。

下面我们看一下多个类型参数的泛型类该如何编写。

例如,我们定义Pair不总是存储两个类型一样的对象,就可以使用类型<T, K>:

public class Pair<T, K> {
private T first;
private K last;

public Pair(T first, K last) {
    this.first = first;
    this.last = last;
}

public T getFirst() { 
	return first;
}

public K getLast() { 
	return last;
}

}
使用的时候,需要指出两种类型:

Pair<String, Integer> p = new Pair<>(“test”, 123);
Java标准库的Map<K, V>就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另一种类型。

小结

编写泛型时,需要定义泛型类型;

泛型可以同时定义多种类型,例如Map<K, V>。

3.2 泛型接口
接口也可以声明泛型。

泛型接口语法形式:

public interface Content {
T text();
}
泛型接口有两种实现方式:

实现接口的子类明确声明泛型类型

预先声明继承的具体类型的接口类,下面就是继承的Integer类型的接口类。

public class IntContent implements Content {
private int text;

public IntContent(int text) {
    this.text = text;
}

@Override
public Integer text() { 
    return text; 
}

}
因为子类并没有泛型类型,所以正常使用就行。

InContent ic = new IntContent(10);
实现接口的子类不明确声明泛型类型

public class GenericsContent implements Content {
private T text;

public GenericsContent(T text) {
    this.text = text;
}

@Override
public T text() { 
    return text; 
}

}
此时子类也使用了泛型类型,就需要指定具体类型

Content gc = new GenericsContent<>(“ABC”);
3.3 泛型方法
泛型方法是引入其自己的类型参数的方法。泛型方法可以是普通方法、静态方法以及构造方法。

泛型方法语法形式如下:

public T func(T obj) {}
注意:是否拥有泛型方法,与其所在的类是否是泛型没有关系。

泛型方法的语法包括一个类型参数列表,在尖括号内,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际类型参数的占位符。

使用泛型方法的时候,通常不必指明类型参数,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。类型推断只对赋值操作有效,其他时候并不起作用。如果将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行推断。

编译器会认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量。

public class GenericsMethodDemo01 {
public static void printClass(T obj) {
System.out.println(obj.getClass().toString());
}

public static void main(String[] args) {
    printClass("abc");
    printClass(10);
}

}
// Output:
// class java.lang.String
// class java.lang.Integer
泛型方法中也可以使用可变参数列表

public class GenericVarargsMethodDemo {
public static List makeList(T… args) {
List result = new ArrayList();
Collections.addAll(result, args);
return result;
}

public static void main(String[] args) {
    List<String> ls = makeList("A");
    System.out.println(ls);
    ls = makeList("A", "B", "C");
    System.out.println(ls);
}

}
// Output:
// [A]
// [A, B, C]
4. 泛型的特性
4.1 类型擦除(Type Erasure)
Java 语言引入泛型是为了在编译时提供更严格的类型检查,并支持泛型编程。不同于 C++ 的模板机制,Java 泛型是使用类型擦除来实现的,使用泛型时,任何具体的类型信息都被擦除了。

那么,类型擦除做了什么呢?它做了以下工作:

把泛型中的所有类型参数替换为 Object,如果指定类型边界,则使用类型边界来替换。因此,生成的字节码仅包含普通的类,接口和方法。
擦除出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get() ;List 就变成了 List。如有必要,插入类型转换以保持类型安全。
生成桥接方法以保留扩展泛型类型中的多态性。类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。
Java 泛型的实现方式不太优雅,但这是因为泛型是在 JDK5 时引入的,为了兼容老代码,必须在设计上做一定的折中。

简单来说类型擦除是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类Pair,这是编译器看到的代码:

public class Pair {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
而虚拟机根本不知道泛型。这是虚拟机执行的代码:

public class Pair {
private Object first;
private Object last;
public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}
public Object getFirst() {
return first;
}
public Object getLast() {
return last;
}
}
因此,Java使用类型擦拭实现泛型,导致了:

编译器把类型视为Object;
编译器根据实现安全的强制转型。
因此,Java使用擦拭法实现泛型,导致了:

编译器把类型视为Object;
编译器根据实现安全的强制转型。
使用泛型的时候,我们编写的代码也是编译器看到的代码:

Pair p = new Pair<>(“Hello”, “world”);
String first = p.getFirst();
String last = p.getLast();
而虚拟机执行的代码并没有泛型:

Pair p = new Pair(“Hello”, “world”);
String first = (String) p.getFirst();
String last = (String) p.getLast();
所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

泛型的局限

了解了Java泛型的实现方式——类型擦除,我们就知道了Java泛型的局限:

局限一:不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:

Pair p = new Pair<>(1, 2); // compile error!
局限二:无法取得带泛型的Class。观察以下代码:

public class test {
public static void main(String[] args) {
List list1 = new ArrayList();
List list2 = new ArrayList();
System.out.println(list1.getClass());
System.out.println(list2.getClass());
}
}
// Output:
// class java.util.ArrayList
// class java.util.ArrayList
因为T是Object,我们对ArrayList和ArrayList类型获取Class时,获取到的是同一个Class,也就是ArrayList类的Class。

换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是ArrayList。

局限三:无法判断带泛型的类型:

List p = new ArrayList<>();
// Compile error:
if (p instanceof List) {
}
原因和前面一样,并不存在List.class,而是只有唯一的List.class。

泛型和继承

正是由于泛型时基于类型擦除实现的,所以,泛型类型无法向上转型。

向上转型是指用子类实例去初始化父类,这是面向对象中多态的重要表现。

20210429172624

Integer 继承了 Object;ArrayList 继承了 List;但是 List 却并非继承了 List。

这是因为,泛型类并没有自己独有的 Class 类对象。比如:并不存在 List.class 或是 List.class,Java 编译器会将二者都视为 List.class。

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

extend通配符

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

// 可以限制传入方法的参数的类型

<? extends xxx> // 也可以限制T的类型 // 类型边界可以设置多个,语法形式如下:

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

不允许调用set(T)方法并传入引用(null除外);
不允许调用T get()方法并获取T引用(只能获取Object引用)。
无界通配符有两种应用场景:

可以使用 Object 类中提供的功能来实现的方法。
使用不依赖于类型参数的泛型类中的方法。
语法形式:<?>

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

public static void main(String[] args) {
    List<Integer> li = Arrays.asList(1, 2, 3);
    List<String> ls = Arrays.asList("one", "two", "three");
    printList(li);
    printList(ls);
}

}
// Output:
// 1 2 3
// one two three
小结

使用类似<? super Integer>通配符作为方法参数时表示:

方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);;
方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();。
即使用super通配符表示只能写不能读。

无限定通配符<?>很少使用,可以用替换,同时它是所有类型的超类。
USB Microphone https://www.soft-voice.com/
Wooden Speakers https://www.zeshuiplatform.com/
亚马逊测评 www.yisuping.cn
深圳网站建设www.sz886.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值