Java 基础1 - 泛型
泛型
泛型是什么?泛型可以理解为一种宽泛的类型,具体是哪一种并没有限制。
泛型类
声明一个容纳 String
类型的 List
可以这样写:
List<String> list = new ArrayList<String>();
这样代表 list
是一个列表,而且是可以容纳 String
类型的,尖括号里面的 String
起到了说明和限制的作用。其实里面不止可以写 String
,还可以写 Integer
或者自定义的其他类甚至是泛型类来作为元素类型,所以 List
是一个泛型容器。
下面展示一个泛型的应用。
public class Pair<T> {
T first;
T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return this.first;
}
public T getSecond() {
return this.second;
}
}
在这里编写 Pair
的时候,Pair
后面加了 <T>
表明这是一个泛型类,它有属性 first
和 second
,这两个属性的类型都是 T
,构造函数的两个变量类型也是 T
,T
可以是随意一种类型(但是当然不能是基本类型啦),而且,Pair
的两个实例方法的返回类型也用了T。
这样写的结果就是:实例化时构造函数传的什么类型,T就是什么类型。接下来试一下:
Pair<Integer> pair = new Pair<Integer>(1, 2);
Integer f = pair.getFirst();
Integer s = pair.getSecond();
System.out.println(f); // 输出1
System.out.println(s); // 输出2
可以看到可以直接用Integer类型的变量来接收 getFirst
和 getSecond
的返回值。可以自行试一下 String
类型的效果。
一个泛型类只能有一个 T
来限制吗?其实可以有多个,接下来改造一下这个 Pair
:
public class Pair<T, V> {
T first;
V second;
public Pair(T first, V second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return this.first;
}
public V getSecond() {
return this.second;
}
}
这次多了个 V
,用它来限制 second
。以下是试用:
Pair<Integer, String> pair = new Pair<>(1, "AAA");
Integer f = pair.getFirst();
String s = pair.getSecond();
System.out.println(f); // 输出1
System.out.println(s); // 输出AAA
注意:其实 T
和 V
的类型可以一样,Pair<String, String> pair = new Pair<>("111", "AAA");
也是可以的。
通过以上的例子可以看出,使用泛型的时候,就是把类型当做参数放进了泛型类后边的那个尖括号中,就像平时调用某个方法时把参数放进了方法的圆括号一样。
其实以上的 Pair
类也可以用不用泛型,first
和 second
用 Object
来表示也是可以实现的,这样代码就会变成:
public class Pair {
Object first;
Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return this.first;
}
public Object getSecond() {
return this.second;
}
}
// 试用
public static void main(String[] args) throws Exception {
Pair pair = new Pair(1, "AAA");
Integer f = (Integer)pair.getFirst();
String s = (String)pair.getSecond();
System.out.println(f); // 输出1
System.out.println(s); // 输出AAA
}
效果确实是一样的,但是可以看到代码中出现了强制类型转换,除了代码变繁琐以外,很明显的一个问题就是万一强转错了就会导致异常出现,然而这肯定是不希望发生的情况。并且,这种问题不会在写代码的时候被 IDE 发现,因为它以为你知道你在做什么。
原理:Java 实现泛型的原理其实也是强制类型转换。对于泛型类,编译器会把泛型代码替换为非泛型代码,将泛型参数T替换为Object,这个过程叫做类型擦除,并加入必要的强制类型转换,等轮到虚拟机执行的时候,是不知道泛型这回事的。
泛型方法
一个方法也可以是支持泛型的,而且跟它所在的类是不是泛型没有关系。
// 泛型方法
public static <T> int indexOf(T[] arr, T elem) {
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(elem)) {
return i;
}
}
return -1;
}
// 试用
public static void main(String[] args) throws Exception {
String[] strs = new String[] { "A", "B", "C" };
int index = indexOf(strs, "C");
System.out.println(index); // 输出2
Integer[] ints = new Integer[] { 1, 2 };
int index2 = indexOf(ints, 1);
System.out.println(index2); // 输出0
}
这里编写了一个支持泛型的方法,在它的返回类型前边加了个<T>
就可以表示这是个泛型方法,这个方法的入参就是泛型的,返回类型为整型。可以看到试用的代码中传入的数组类型是可以不限定的。
泛型接口
有了前边的基础,可以直接上代码:
// 接口类Compare
package Interfaces;
public interface Compare<T> {
boolean eq(T target);
}
// People类
import Interfaces.Compare;
public class People implements Compare<People> {
private String name;
public People(String name) {
this.name = name;
}
@Override
public boolean eq(People target) {
return this.name.equals(target.name);
}
}
// 测试
public class App {
public static void main(String[] args) throws Exception {
People p1 = new People("A");
People p2 = new People("A");
People p3 = new People("B");
System.out.println(p1.eq(p2)); // TRUE
System.out.println(p1.eq(p3)); // FALSE
}
}
这声明了一个 Compare
接口,它有一个 eq()
方法用于判断两个对象是否相等,至于对象的具体类型是未知的,使用泛型 T
来代表。
People
类实现了这个接口,注意实现的时候在 Compare
后边尖括号指明了是 People
类,所以在实现eq()
方法的时候,参数传的就是一个 People
类型的 target
。
泛型的限定
Basic
前边写的泛型都是代表可以传入任意类型,但是其实是不安全的,假如有个泛型方法涉及对象中 length
的访问,这个方法很明显不能适用于 Integer
类型的对象,因为它没有这个属性,这时候需要对泛型对象进行类型的限制。
类型限制是有上界和下界的,可以理解为范围。
-
上界示例
public class App { public static void main(String[] args) throws Exception { XM xm = new XM(1); People p = new People(2); add(xm, p); // 输出3 Integer a = new Integer(1); add(xm, a); // 报错:reason: no instance(s) of type variable(s) exist so that Integer conforms to People } public static <T extends People> void add(T a, T b) { System.out.println(a.getAge() + b.getAge()); } } class XM extends People{ public XM(int age) { super(age); } } class People { private int age; public People(int age) { this.age = age; } public int getAge() { return this.age; } }
以上代码中,
add()
方法前边的泛型写成了:<T extends People>
,代表T所能支持的类型范围最高只能到People
。它的意思是,T
这个类型是People
或者People
的子类,不能是其他的。XM
是People
的子类,所以XM
和People
的对象都可以传入add
方法内计算,而Integer
就会报错说不能转换成People
。 -
下界示例
详见通配符。
通配符
通配符是一种简写的方式。
1.<?>
比如以下两端代码效果是一样的:
public static <T> void outAll(List<T> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.getClass());
}
}
public static void outAll(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.getClass());
}
}
通配符的看上去比类型参数要简洁一点。
2.<? super E>
关于泛型限定请看:
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) throws Exception {
List<XM> xmList = new ArrayList<>();
XM xm = new XM(1);
xmList.add(xm);
outAll(xmList); // 输出1
List<People> peopleList = new ArrayList<>();
People p = new People(2);
peopleList.add(p);
outAll(peopleList); // 输出2
}
public static void outAll(List<? extends People> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i).getAge());
}
}
// public static <T extends People> void outAll(List<T> list) {
// for (int i = 0; i < list.size(); i++) {
// System.out.println(list.get(i).getAge());
// }
// }
}
这里写了两个 List
,一个装 XM
,一个装 People
。在 outAll()
方法的入参用了 <? extends People>
,它也可以限定元素类型为 People
或者其子类。
其实,这里完全可以用上一节的写法代替本节的通配符写法(比如注释那一段),只是通配符可以更简洁一点。然而并不是所有通配符写法都可以被这样替换的,详情见超类型通配符。
3.<? super E>超类型通配符
这个就有点下界的意思了。
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) throws Exception {
List<XM> xmList = new ArrayList<>();
XM xm = new XM(1);
xmList.add(xm);
outAll(xmList); // 输出class XM
List<People> peopleList = new ArrayList<>();
People p = new People(2);
peopleList.add(p);
outAll(peopleList); // 输出class People
Object o = new Object();
List<Object> list = new ArrayList();
list.add(o);
outAll(list); // 输出class java.lang.Object
XMM xmm = new XMM(3); // XMM 继承了XM
List<XMM> xmmList = new ArrayList();
list.add(xmm);
outAll(xmmList); // 报错
}
public static void outAll(List<? super XM> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i).getClass());
}
}
}
这里把 extends
换成了 super
,代表 List
中的元素类型只能是 XM
或者 XM
的父类,不能再比 XM
低了,所以对于 XMM
这个继承了 XM
的类来讲就会报错,方法不接受这种类型的集合。
看个例子:
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) throws Exception {
List<Parent> pList = new ArrayList<>();
pList.add(new Parent("p1"));
List<Child> cList = new ArrayList<>();
cList.add(new Child("c1"));
outAll(pList); // OK
outAll(cList); // 报错
}
public static <T extends Compare<T>> void outAll(List<T> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.getClass());
}
}
}
class Parent implements Compare<Parent> {
public String name;
public Parent(String name) {
this.name = name;
}
@Override
public boolean eq(Parent target) {
return this.name.equals(target.name);
}
}
class Child extends Parent {
public Child(String name) {
super(name);
}
}
interface Compare<T> {
boolean eq(T target);
}
父类实现了 Compare
接口,子类也可以调用接口内同名的方法,但是在 outAll()
方法却不接受 List<Child>
这中类型的参数,原因就在于 <T extends Compare<T>>
的意思是:T
实现了 Compare
接口,而且是和同种类型作比较。
父类的实现是 Compare<Parent>
而子类却没有实现 Compare<Child>
所以不被接受,子类实现的其实也是 Compare<Parent>
。
解决办法就是把 <T extends Compare<T>>
改为 <T extends Compare<? super T>>
,改过之后就可以接纳类实现 Compare<Parent>
这种情况了。可以看到通配符写法也并不总是可以被替换的。
总结
上述三种的区别如下:
- 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
- 和用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,它们可以用类型参数的形式替代,但通配符形式更为简洁。
- 可以去找些容器类研究下
ATTENTION
-
获取类的信息
可以使用
Integer.class
但不能使用Pair<integer>.class
。因为这个类型对象只有一份,与泛型无关。以下可以验证:Pair<Integer, Integer> pair1 = new Pair<>(1, 2); Pair<Integer, String> pair2 = new Pair<>(1, "AAA"); System.out.println(pair1.getClass() == Pair.class); // true System.out.println(pair2.getClass() == Pair.class); // true
-
instanceof
不适用于泛型以下是不支持的:
pair1 instanceof Pair<Integer, Integer>;
-
接口
之前有过两个类,父类的实现是
Compare<Parent>
,子类实现的其实也是Compare<Parent>
。此时想让子类再实现这个接口是不行的,下面的代码是会报错的:class Child extends Parent implements Compare<Child> { public Child(String name) { super(name); } }
Compare
接口不能被实现两次,且两次实现的类型参数还不同,一次是Compare<Parent>
,一次是Compare<Child>
。为什么不允许呢?因为类型擦除后,实际上只能有一个。解决办法是直接重写父类的
eq()
方法即可。 -
泛型类型不能实例化
某个参数是
T
类型的,不可以通过new T()
来实例化。 -
不能创建泛型类数组
以下是非法的:
Pair<Integer, Integer>[] arr = new Pair<Integer, Integer>[] { new Pair<>(1, 2), new Pair<>(3, 4) };
原因如下:
Integer[] ints = new Integer[2]; Object[] objs = ints; objs[0] = "aaa"; // 编译是没有问题的,运行时会抛出ArrayStoreException,因为Java知道实际的类型是Integer,所以写入String会抛出异常。 Pair<Integer, Integer>[] arr = new Pair<Integer, Integer>[] { new Pair<>(1, 2), new Pair<>(3, 4) }; Object[] objs2 = arr; objs2[0] = new Pair<Double, String>(3.1d, "aaa"); // 这里在运行的时候什么错误都不会发生
因为
Pair<Double, String>
的运行时类型是Pair
,和objs2
的运行时类型Pair[]
是匹配的。但其实它的实际类型是不匹配的,在程序的其他地方,当把objs2[0]
作为Pair<Integer, Integer>
进行处理的时候,一定会触发异常。可以使用原始类型创建数组存放泛型对象:
Pair[] arr = new Pair[] { new Pair<Integer, Integer>(1, 2), new Pair<Double, String>(3.1d, "aaa") };
本文作者:为何匆匆
本文链接:https://www.cnblogs.com/nyfblog/p/16519103.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步