> Java > java지도 시간 > 본문

Java 제네릭에 대한 포괄적이고 자세한 설명

黄舟
풀어 주다: 2017-03-01 13:13:31
원래의
1586명이 탐색했습니다.

우리는 일상생활에서 제네릭을 자주 사용하지만 제네릭 데이터에서 설명할 수 없는 오류가 보고되는 경우도 있고, 와일드카드와 같은 일부 구문과 가상 머신에서 제네릭의 실제 작동 방법도 연구할 가치가 있으므로 오늘은 제네릭에 대해 함께 논의하겠습니다. .

(1) 생성

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 배열에 저장된 유형은 String이며 ArrayList와 유사한 add 메소드도 자동으로 String 유형을 연결합니다. 다른 유형의 값은 목록에 추가할 수 없습니다.

그리고 한 가지 지적하고 싶은 점은 위 코드의 처음 두 줄에는 오류가 없다는 점입니다. ArrayList에서는 <>를 사용하지 않아도 괜찮기 때문에 제네릭 클래스에는 여전히 많은 제한이 있으므로 필요한 경우 ArrayList 클래스도 사용합니다.

(2) 제네릭 클래스

간단한 제네릭 클래스를 직접 들어보겠습니다.

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만 작성하면 됩니다. 두 필드 T와 U는 각각 다른 유형을 사용합니다.

위의 예는 너무 단순해서 실질적인 작업을 수행할 수 없습니다. 따라서 위의 제네릭 클래스에 함수를 추가한다면 다음과 같이 생각해보세요.

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>

(3 ) 제네릭 메소드

1. 일반 클래스의 제네릭 메소드

제네릭 클래스에 정의된 제네릭은 클래스 전체에서 유효합니다. 즉, 제네릭 클래스의 객체가 연산할 특정 유형을 결정한 후에는 연산할 모든 유형이 이미 고정되어 있습니다. 예를 들어, 전체 ArrayList의 일반 유형인 ArrayList은 String 유형에 바인딩되어 수정될 수 없습니다. 그러나 어떤 경우에는 메소드가 다양한 불확실한 유형에 대해 작동할 수 있도록 더 많은 유연성이 필요하며 메소드에 제네릭을 정의할 수 있습니다.

    public <T> T getMid(T[] array){        
    return array[array.length/2];
    }
로그인 후 복사

예를 들어 위의 getMid 함수에서 반환 값 T 앞에

를 추가하면 일반 메서드로 전환됩니다. <T>일반 클래스에 있으며 제네릭 클래스에 제네릭 메서드가 반드시 존재할 필요는 없다는 점에 유의하세요. 다음은 제네릭 메서드 호출 시 컴파일러가 다음 유형을 기반으로 호출된 메서드를 추론하므로 T 유형이 무엇인지 명시적으로 표시할 필요가 없습니다.

String mid = ArrayAlg.getMid("Jone" , "Q" , "Peter");
//okdouble mid2 = ArrayAlg.getMid(3.14 , 25.9 , 20);
//error,编译器会把20自动打包成Integer类型,而其他打包成Double类型,我们尽量不要让这种错误发生
로그인 후 복사

2. 제네릭 클래스의 제네릭 메소드

일반 클래스에서는 위의 제네릭 메소드를 통해 다양한 타입의 매개변수를 유연하게 호출할 수 있습니다. 제네릭 클래스에서는 이러한 유연한 제네릭 메서드가 필요합니까? 나는 그것이 필요하지 않다고 생각한다. 우리는 일반 클래스에 대한 유형 매개변수를 제공합니다. 실제로 한 유형에 다른 유형을 사용하는 경우는 거의 없습니다. 유형을 사용하려면 일반 클래스를 정의하십시오.

그럼 제네릭 클래스에서 제네릭 메서드를 사용하는 방법은 무엇인가요? 제네릭 클래스에서 제네릭 메서드를 사용하여 제네릭 변수를 제한할 수 있습니다. 예를 들어 위에서 언급한
T 메서드는 Comparable 인터페이스가 있는 T에서만 사용할 수 있습니다. 그렇지 않으면 오류가 발생합니다. <T extends Comparable>

@SuppressWarnings("hiding")
public static <T extends Comparable<?> & Serializable> T min(T[] array) {...}
로그인 후 복사

(四) 虚拟机中的泛型

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

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类型。

2.翻译泛型方法,桥方法被使用以保持多态性

对于类型擦出也出现在泛型方法中,但是泛型方法中的擦除带来很多问题:

//----------------类A擦除前----------------------class A extends Pair<Date>{
    public void setSecond(Date second){...}
}//----------------类A擦除后----------------------class A extends Pair{
    public void setSecond(Date second){...}
}
로그인 후 복사

从上面的代码中,我们可以看出,类A重写了Pair中的setSecond方法,看似合情合理,但是一旦擦除之后就发生了问题。将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参数的方法,从而将两个方法合二为一,这个桥方法不是我们自己写的,而是在虚拟机中自动生成的,让代码变得安全,让运行结果变得符合我们的期望。

(五) 通配符 “?”

当两个有关系的类分别作为两个泛型类的类型变量的时候,这两个泛型类是没有关系的。这个时候如果需要涉及到继承规则之类的内容时,那么就需要使用通配符——“?”。

1. 子类型限定和超类型限定(上下界限定)

有的时候两个类间有继承关系,但是分别作为泛型类型变量之后就没了关系,在函数调用和返回值的时候,这种不互通尤为让人头痛。好在通配符的上下界限定类型为我们安全的解决了这个难题。
? 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的方法才是转换安全的。现在我们可以试着去理解上面的>了。

2. 无限定通配符

无限定通配符的用法其实很单一。无限定通配符修饰的返回值只能为Object,而其做参数是不可以的。那么无限定通配符的用处在哪里呢?源于他的可读性好。

public static boolean hasNulls(Pair<?>){...}
로그인 후 복사

上面的代码的意思是对于任何类型的Pair泛型判断是否为空,这样写比用T来表示可读性确实好的多。

(六) 泛型注意事项

1. 不能用基本类型实例化类型参数

八大基本类型务必要使用包装类来实例化,否则泛型参数一擦除我们就傻眼了,怎么把int的数据放到Object里面呢?

2. 不能创建参数化类型数组

参数化的类型数组是不能被创建的。我们完全可以用多个泛型嵌套来避免这种情况的发生,如果创建了泛型数组,擦除之后类型会变成Object[ ],如果有一个类型参数不同的泛型存入这个数组时,因为都被擦除成Object,所以不会报错,导致了错误的发生。需要说明的是,只是不允许创建这些数组,而声明类型为Pair[]的变量是合法的,只不过不能用new Pair[10]初始化这个变量。更确切地表达是:数组类型不能是类型变量,采用通配符的方式是可以允许的

3. 运行时类型查询只适用于原始类型

要记住在虚拟机中,每个对象都有一个特定的非泛型类。所以,所有的类型查询只产生原始类型。比如:

if(a instanceof Pair<String>) //只能测试a是否是任意类型的一个Pair
Pair<String> sp = ...;
sp.getClass(); //获得的也是Pair(原始类型)
로그인 후 복사

4. 不能实例化类型变量

有的时候我们需要将类型变量实例化成它自己本身的类型,但是一定要注意写法,不可以直接实例化类型变量:

public Pair() {    this.first = new T();//错误,类型擦除后T变成Object,new Object()肯定不是想要的
    this.first = T.class.newInstance();//错误,T.class是不合法的写法}
로그인 후 복사

上述的两种写法都是错的,如果一定要这样做的话,也只能在调用泛型类的时候构造出一个方法(构造的方法不在泛型类中,在调用泛型类的普通类中),将你要用的类型作为参数传进去,想办法来实例化。

5. 泛型类的静态上下文中类型变量无效

不能在静态域中引用类型变量,静态方法本来就是和对象无关,他怎么能知道你传进来的是什么类型变量呢?

6. 不能抛出或者捕获泛型类实例

既不能抛出也不能捕获泛型对象。事实上,甚至泛型类扩展Throwable都是不可以的,但是平时我们不去编写泛型类的时候,这一条并不需要注意过多。

7. 继承关系的类分别成为类型参数后无关系

当两个有关系的类分别作为两个泛型类的类型变量的时候,这两个泛型类是没有关系的。这个时候如果需要涉及到继承规则之类的内容时,一定要使用通配符,坚决不要手软。

8. 泛型类的静态方法必须作为泛型方法

泛型类的静态方法因为是静态,所以也不能获得类型变量,在这个时候唯一的解决办法是——所有的静态方法都是泛型方法。

9. 通配符不能作为一种类型使用

? t = p.getA(); //error
로그인 후 복사

所有用“?”来作为类型的写法都是不被允许的,我们需要用T来作为类型参数,有必要的时候可以做一个辅助函数,用T类型来完成工作,用?通配符来做外面的包装,既达到了目的,又提高了可读性。

(七)  总结

泛型为我们提供了许许多多的便利,包装出来的很多泛型类让我们能更快速安全的工作。而泛型的底层实现原理也是很有必要研究一下的,一些需要我们注意的事项和通配符的使用,此类细节也确实有许多值得我们学习之处。

在日常生活中,我们经常用到泛型,但是泛型数据有些时候会报一些莫名其妙的错,而且一些通配符等语法、泛型在虚拟机中的真正操作方式也有我们值得研究之处,今天我们就一起来讨论一下泛型。

 以上就是java泛型综合详解的内容,更多相关内容请关注PHP中文网(www.php.cn)!


관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!