우리는 일상생활에서 제네릭을 자주 사용하지만 제네릭 데이터에서 설명할 수 없는 오류가 보고되는 경우도 있고, 와일드카드와 같은 일부 구문과 가상 머신에서 제네릭의 실제 작동 방법도 연구할 가치가 있으므로 오늘은 제네릭에 대해 함께 논의하겠습니다. .
Java에 제네릭이 추가되기 전에는 현재 제네릭으로 작동하는 프로그램을 처리하기 위해 상속을 사용했습니다.
ArrayList files = new ArrayList();String filename = (String) files.get(0); ArrayList<String> files2 = new ArrayList<>(); //后一个尖括号中的内容可以省略,在1.7之后String filename2= files2.get(0);String addname = "addname"; files2.add(addname); //在add函数调用之时,ArrayList泛型会自动检测add的内容是不是String类型 files2.add(true); //报错 The method add(String) in the type ArrayList<String> is not applicable for the arguments (boolean)
예를 들어 ArrayList 클래스는 객체 참조 배열만 유지합니다. 값(위의 처음 두 줄)을 가져올 때 유형 변환이 필요하며 콘텐츠를 전달할 때는 보장되지 않습니다. 제네릭을 사용하면 이 데이터 구조가 편리해지고 읽기 쉽고 안전해집니다. ArrayList
그리고 한 가지 지적하고 싶은 점은 위 코드의 처음 두 줄에는 오류가 없다는 점입니다. ArrayList에서는 <>를 사용하지 않아도 괜찮기 때문에 제네릭 클래스에는 여전히 많은 제한이 있으므로 필요한 경우 ArrayList 클래스도 사용합니다.
간단한 제네릭 클래스를 직접 들어보겠습니다.
public class Pair<T> { private T first; private T second; public Pair() { first = null; second = null; } 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; } }
사실 제네릭 클래스도 일반 클래스로 간주할 수 있습니다. , T를 필요한 유형으로 바꾸십시오. 그러면 두 가지 유형이 필요한 상황에서는 pair
위의 예는 너무 단순해서 실질적인 작업을 수행할 수 없습니다. 따라서 위의 제네릭 클래스에 함수를 추가한다면 다음과 같이 생각해보세요.
public T min(T[] array) {...}//返回传入数组中的最小值
그렇다면, 이 기능을 구현하려면 최소한 T 유형 개체의 크기를 비교할 수 있는지 확인해야 합니다. 실제로 유형 변수 T에 제한을 설정하여 이 문제를 해결할 수 있습니다.
public <T extends Comparable>T min(T[] array){...}//T如果没有实现compare方法则报错不执行min函数
<T extends Comparable>
의 의미는 거의 짐작할 수 있습니다. 즉, Comparable 인터페이스를 상속하려면 유형 변수 T가 필요하다는 것입니다. 이제 일반 min 메소드는 Comparable 인터페이스(예: String, Date...)를 구현하는 클래스 배열에 의해서만 호출될 수 있으며 다른 클래스는 컴파일 오류를 생성합니다.
마찬가지로 여러 가지 제한사항이 있을 수 있습니다. <T,U extends Comparable&Serializable>
도 허용됩니다. 쉼표는 유형 변수를 구분하는 데 사용되며 &는 정규화된 유형을 구분하는 데 사용됩니다.
사실 Comparable 자체는 제네릭 인터페이스가 아니라 위의 내용을 쉽게 이해하기 위해 헷갈리는 척하고 있습니다. 그러나 위 내용을 작성하는 실제 올바른 방법은 다음과 같습니다. <T extends Comparable>
public <T> T getMid(T[] array){ return array[array.length/2]; }
를 추가하면 일반 메서드로 전환됩니다. <T>
일반 클래스에 있으며 제네릭 클래스에 제네릭 메서드가 반드시 존재할 필요는 없다는 점에 유의하세요. 다음은 제네릭 메서드 호출 시 컴파일러가 다음 유형을 기반으로 호출된 메서드를 추론하므로 T 유형이 무엇인지 명시적으로 표시할 필요가 없습니다.
String mid = ArrayAlg.getMid("Jone" , "Q" , "Peter"); //okdouble mid2 = ArrayAlg.getMid(3.14 , 25.9 , 20); //error,编译器会把20自动打包成Integer类型,而其他打包成Double类型,我们尽量不要让这种错误发生
그럼 제네릭 클래스에서 제네릭 메서드를 사용하는 방법은 무엇인가요? 제네릭 클래스에서 제네릭 메서드를 사용하여 제네릭 변수를 제한할 수 있습니다. 예를 들어 위에서 언급한
T 메서드는 Comparable 인터페이스가 있는 T에서만 사용할 수 있습니다. 그렇지 않으면 오류가 발생합니다. <T extends Comparable>
@SuppressWarnings("hiding") public static <T extends Comparable<?> & Serializable> T min(T[] array) {...}
java虚拟机中没有泛型类型对象——所有的对象都属于普通类,那么虚拟机是怎么用普通类来模拟出泛型的效果呢?
只要定义了一个泛型类,虚拟机都会自动的提供一个原始类型。原始类型的名字就是删除类型参数的泛型类的名字。擦除类型变量,并替换为第一个限定类型(如果没有限定则用Object来替换)。
比如,我们在一开始给出的简单泛型类Pair<T>在虚拟机中会变成如下的情况:
public class Pair { private Object first; private Object second; public Pair() { first = null; second = null; } public Pair(Object first, Object second) { this.first = first; this.second = second; } public Object getFirst() { return first; } public void setFirst(Object first) { this.first = first; } public Object getSecond() { return second; } public void setSecond(Object second) { this.second = second; } }
这就是擦除了类型变量,并且将没有限制的T换成Object之后的情况,如果有限定类型,比如,我们的泛型类是Pair
public class Pair { private Compararble first; private Compararble second; public Pair() { first = null; second = null; } public Pair(Compararble first, Compararble second) { this.first = first; this.second = second; } public Compararble getFirst() { return first; } public void setFirst(Compararble first) { this.first = first; } public Compararble getSecond() { return second; } public void setSecond(Compararble second) { this.second = second; } }
那么既然类型都被擦除了,类型参数也被替换了,怎么起到泛型的效果呢?当程序调用泛型方法时,如果返回值是类型参数(就是返回值是T),那么编译器会插入强制类型转换:
Pair<String> a = ...; String b = a.getFirst();//编译器强制将a.getFirst()的Object类型的结果转换为String类型
编译器将上述过程翻译位两条虚拟机指令:
- 对原始方法Pair.getFirst的调用。
- 将返回的Object类型强制转换为String类型。
对于类型擦出也出现在泛型方法中,但是泛型方法中的擦除带来很多问题:
//----------------类A擦除前----------------------class A extends Pair<Date>{ public void setSecond(Date second){...} }//----------------类A擦除后----------------------class A extends Pair{ public void setSecond(Date second){...} }
从上面的代码中,我们可以看出,类A重写了Pair
public Class Pair{ public void setSecond(Object second){...} }
突然发现,父类Pair中的setSecond方法参数变为Object,和子类中的参数不同。擦除之后,父类的setSecond方法和子类的setSecond方法完全变为两个不一样的方法!这就没有了重写之说,那么接着考虑下面的语句:
A a = new A();Pair<Date> pair = a;pair.setSecond(new Date()); //当pair去调用setSecond函数时,有两个不一样的setSecond函数可以被调用,一个是父类中参数为Object的,一个是子类中参数为Date的。
第三行的调用出现了两种情况,这绝对不是我们想要的结果,我们开始的时候只是重写了父类中的setSecond方法,但是现在有两个不同的setSecond方法可以被使用,而且这时编译器不知道要去调用哪个。
为了防止这种情况的发生,编译器会在A类(子类)中生成一个桥方法:
//------------桥方法-------------- public void setSecond(Object second){ setSecond((Date)second); }
这个桥方法,让Object参数的方法去调用Date参数的方法,从而将两个方法合二为一,这个桥方法不是我们自己写的,而是在虚拟机中自动生成的,让代码变得安全,让运行结果变得符合我们的期望。
当两个有关系的类分别作为两个泛型类的类型变量的时候,这两个泛型类是没有关系的。这个时候如果需要涉及到继承规则之类的内容时,那么就需要使用通配符——“?”。
有的时候两个类间有继承关系,但是分别作为泛型类型变量之后就没了关系,在函数调用和返回值的时候,这种不互通尤为让人头痛。好在通配符的上下界限定类型为我们安全的解决了这个难题。 ? super A
的意思是“?是A类型的父类型”,? extends A
的意思是“?是A类型的子类型”。
既然知道了意思,我们看一下下面这四种用法:
public static void printA (Pair<? super A> p) {...} //ok public static void printA (Pair<? extends A> p) {...} //error public static Pair<? extends A> printA () {...} //ok public static Pair<? super A> printA () {...} //error
为什么四种中有两种有错误呢?
实际上,要牢记这句话:带有超类型限定的通配符可以向泛型对象写入(做参数),带有子类型限定的通配符可以从泛型对象读取(做返回值)。道理这里就不详细讲了,如果有兴趣研究的话可以思考一下,思考的方向无非是继承关系之间的引用转换,只有上面两行ok的方法才是转换安全的。现在我们可以试着去理解上面的
无限定通配符的用法其实很单一。无限定通配符修饰的返回值只能为Object,而其做参数是不可以的。那么无限定通配符的用处在哪里呢?源于他的可读性好。
public static boolean hasNulls(Pair<?>){...}
上面的代码的意思是对于任何类型的Pair泛型判断是否为空,这样写比用T来表示可读性确实好的多。
八大基本类型务必要使用包装类来实例化,否则泛型参数一擦除我们就傻眼了,怎么把int的数据放到Object里面呢?
参数化的类型数组是不能被创建的。我们完全可以用多个泛型嵌套来避免这种情况的发生,如果创建了泛型数组,擦除之后类型会变成Object[ ],如果有一个类型参数不同的泛型存入这个数组时,因为都被擦除成Object,所以不会报错,导致了错误的发生。需要说明的是,只是不允许创建这些数组,而声明类型为Pair
要记住在虚拟机中,每个对象都有一个特定的非泛型类。所以,所有的类型查询只产生原始类型。比如:
if(a instanceof Pair<String>) //只能测试a是否是任意类型的一个Pair Pair<String> sp = ...; sp.getClass(); //获得的也是Pair(原始类型)
有的时候我们需要将类型变量实例化成它自己本身的类型,但是一定要注意写法,不可以直接实例化类型变量:
public Pair() { this.first = new T();//错误,类型擦除后T变成Object,new Object()肯定不是想要的 this.first = T.class.newInstance();//错误,T.class是不合法的写法}
上述的两种写法都是错的,如果一定要这样做的话,也只能在调用泛型类的时候构造出一个方法(构造的方法不在泛型类中,在调用泛型类的普通类中),将你要用的类型作为参数传进去,想办法来实例化。
不能在静态域中引用类型变量,静态方法本来就是和对象无关,他怎么能知道你传进来的是什么类型变量呢?
既不能抛出也不能捕获泛型对象。事实上,甚至泛型类扩展Throwable都是不可以的,但是平时我们不去编写泛型类的时候,这一条并不需要注意过多。
当两个有关系的类分别作为两个泛型类的类型变量的时候,这两个泛型类是没有关系的。这个时候如果需要涉及到继承规则之类的内容时,一定要使用通配符,坚决不要手软。
泛型类的静态方法因为是静态,所以也不能获得类型变量,在这个时候唯一的解决办法是——所有的静态方法都是泛型方法。
? t = p.getA(); //error
所有用“?”来作为类型的写法都是不被允许的,我们需要用T来作为类型参数,有必要的时候可以做一个辅助函数,用T类型来完成工作,用?通配符来做外面的包装,既达到了目的,又提高了可读性。
泛型为我们提供了许许多多的便利,包装出来的很多泛型类让我们能更快速安全的工作。而泛型的底层实现原理也是很有必要研究一下的,一些需要我们注意的事项和通配符的使用,此类细节也确实有许多值得我们学习之处。
在日常生活中,我们经常用到泛型,但是泛型数据有些时候会报一些莫名其妙的错,而且一些通配符等语法、泛型在虚拟机中的真正操作方式也有我们值得研究之处,今天我们就一起来讨论一下泛型。
以上就是java泛型综合详解的内容,更多相关内容请关注PHP中文网(www.php.cn)!