자바 프로그래밍 사고 학습 수업(3) 15장 - 제네릭
제네릭의 개념 (제네릭)은 Java SE5의 주요 변경 사항 중 하나입니다. 제네릭은 매개변수화된 유형(매개변수화된 유형) 개념을 구현하여 코드를 여러 유형에 적용할 수 있도록 합니다. "일반"이라는 용어는 "많은 유형에 적용 가능"을 의미합니다.
1 제네릭 메서드
제네릭 메서드가 있는 클래스가 제네릭인지 여부와는 아무런 관련이 없습니다. 즉, 제네릭 메서드가 있는 클래스가 제네릭 클래스일 수도 있고 아닐 수도 있습니다.
일반 메소드를 사용하면 클래스와 독립적으로 메소드를 변경할 수 있습니다.
기본 원칙: 가능하다면 일반적인 방법을 사용해야 합니다. 즉, 일반 메서드를 사용하여 전체 클래스를 일반화하는 것을 대체할 수 있다면 상황이 더 명확해지므로 일반 메서드만 사용해야 합니다.
static
메서드의 경우 일반 클래스의 유형 매개변수에 액세스할 수 없습니다. 따라서static
메서드가 일반 기능을 사용해야 하는 경우 Generic이어야 합니다. 행동 양식.static
方法而言,无法访问泛型类的类型参数,所以,如果static
方法需要使用泛型能力,就必须使其成为泛型方法。要定义泛型方法,只需将泛型参数列表置于返回值之前。
1.1 类型参数推断
使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。
类型推断只对赋值操作有效。
如果将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断。
1.1.1 显式的类型说明
在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内,即显式的类型说明。
2 擦除的神秘之处
根据JDK文档的描述,
Class.getTypeParameters()
将“返回一个TypeVariable
对象数组,表示有泛型声明的类型参数…..”,这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中看到,你能够发现的只是用作参数占位符的标识符,这并非有用的信息。因此,残酷的现实是:在泛型代码内部,无法获得任何有关泛型参数类型的信息。
因此,你可以知道诸如泛型参数标识符和泛型类型边界这类信息——你却无法知道创建某个特定实例的实际的类型参数。……,在使用Java泛型工作时它是必须处理的最基本的问题。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此
List<String>
和List<Integer>
在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生类型,即List
。
2.1 C++的方式
2.1.1 以下C++模板示例:
일반 메소드를 사용할 때 일반적으로 매개변수 유형을 지정할 필요는 없습니다. 컴파일러가 특정 유형을 찾아주기 때문입니다. 이를 형식 인수 추론이라고 합니다.它怎么知道
f()
方法是为类型参数T而存在的呢?当你实例化这个模板时,C++编译器将进行检查,因此在Manipulator<HasF>
被实例化的这一刻,它看到HasF
拥有一个方法f()
일반 메서드를 정의하려면 반환 값 앞에 일반 매개변수 목록을 넣으면 됩니다. 1.1 유형 매개변수 추론
유형 추론은 할당 작업에만 유효합니다.
일반 메소드 호출의 결과가 매개변수로 다른 메소드에 전달되면 컴파일러는 유형 추론을 수행하지 않습니다.
2 삭제의 미스터리 🎜🎜🎜JDK 문서에 따르면 🎜1.1.1 명시적 유형 지정
도트 연산자와 메소드 이름 사이에 꺾쇠괄호를 삽입한 다음 꺾쇠괄호 안에 유형을 배치합니다. 즉, Explicit 유형 설명 .Class.getTypeParameters()
🎜는 "There를 나타내는 🎜TypeVariable
🎜 객체 배열을 반환합니다. 일반 선언된 유형 매개변수는...'입니다. 이는 매개변수 유형에 대한 정보를 찾을 수 있음을 암시하는 것처럼 보이지만 출력에서 볼 수 있듯이 찾을 수 있는 것은 🎜🎜매개변수 자리표시자🎜🎜로 사용되는 것뿐입니다. 식별자 🎜🎜는 유용한 정보가 아닙니다. 🎜🎜가혹한 현실은 다음과 같습니다. 🎜🎜일반 코드 내에서는 일반 매개변수 유형에 대한 정보를 얻을 수 있는 방법이 없습니다🎜🎜. 🎜🎜따라서 🎜일반 매개변수 식별자🎜 및 🎜일반 유형 경계🎜와 같은 것을 알 수 있지만 🎜특정 인스턴스를 생성하는 실제 유형 매개변수🎜는 알 수 없습니다. ..., 이는 Java Generics로 작업할 때 🎜다뤄야 하는 🎜가장 기본적인 🎜문제🎜입니다. 🎜🎜Java 제네릭은 🎜🎜erasure🎜🎜를 사용하여 구현됩니다. 즉, 제네릭을 사용할 때 모든 특정 유형 정보가 지워집니다. 🎜알 수 있는 유일한 것은 객체를 사용하고 있다는 것🎜입니다. 따라서 🎜List<string></string>
🎜과 🎜List<integer>🎜는 실제로 런타임 시 🎜🎜동일한 유형🎜🎜입니다. 두 양식 모두 🎜<code>List
🎜인 "🎜🎜네이티브 유형🎜🎜으로 지워집니다. 🎜🎜🎜2.1 C++ 방식 🎜🎜 2.1.1 다음 C++ 템플릿 예: 🎜🎜 🎜방법 이 템플릿을 🎜인스턴스화🎜하면 C++ 컴파일러가 이를 확인하므로Manipulator<HasF>? code>🎜🎜인스턴스화되는 순간🎜🎜은 <code>HasF
에f()
메서드가 있음을 확인합니다. 컴파일 시간 오류🎜🎜, 🎜유형 안전성이 보장됩니다🎜. 🎜🎜🎜 2.1.2는 삭제로 인해 컴파일되지 않으므로 Java 컴파일러는 obj 맵에서 f()를 호출할 수 없습니다. HasF가 f() 🎜🎜🎜2.2 일반 경계 🎜🎜🎜f()를 호출하려면 🎜🎜일반 클래스의 경계 🎜🎜를 고려하여 일반 클래스를 🎜🎜assist🎜🎜해야 합니다. 🎜이 경계를 따르는 유형🎜만 허용할 수 있다고 컴파일러에 알립니다. 경계 덕분에 다음 코드를 컴파일할 수 있습니다🎜.// Templates.cpp#include <iostream>using namespace std;template<class T> class Manipulator{ T obj;public: Manipulator(T x) { obj = x; } void manipulate() { obj.f(); } };class HasF{public: void f() { cout << "HasF::f()" << endl; } };int main(){ HasF hf; Manipulator<HasF> manipulator(hf); manipulator.manipulate(); }로그인 후 복사
package net.mrliuli.generics.erase;/** * Created by li.liu on 2017/12/7. *//** * 由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。 * @param <T> */class Manipulator<T>{ private T obj; public Manipulator(T x){ obj = x; } // Error: Cannot resolve method 'f()' //public void manipulate(){ obj.f(); }}/** * 为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。由于有了边界,下面的代码就可以编译了。 * @param <T> */class Manipulator2<T extends HasF>{ private T obj; public Manipulator2(T x){ obj = x; } public void manipulate(){ obj.f(); } }public class Manipulation { public static void main(String[] args){ HasF hf = new HasF(); Manipulator<HasF> manipulator = new Manipulator<>(hf); //manipulator.manipulate(); Manipulator2<HasF> manipulator2 = new Manipulator2<>(hf); manipulator2.manipulate(); } }
2.3 擦除
我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。
T
擦除到了HasF
,就好像在类的声明中用HasF
替换了T
一样。
2.4 擦除的问题
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为迁移兼容性。
因此,擦除主要的正当理由是从非泛化的代码到泛化的代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。
擦除的代码是显著的。
如果编写了下面这样的代码:
class Foo<T>{ T var; }
那么看起来当你在创建Foo
的实例时:
Foo<Cat> f = new Foo<Cat>();
class Foo
中的代码应该知道现在工作于Cat
之上,而泛型语法也强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实上并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object。”擦除和迁移兼容性意味着,使用泛型并不是强制的。
class GenericBase<T>{}class Derived1<T> extends GenericBase<T>{}class Derived2 extends GenericBase{} // No warning
2.5 边界处的动作
即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。
因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。
在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方。”
3 擦除的补偿(Compensating for erasure)
有时必须通过引入类型标签(type tag)来对擦除进行补偿(compensating)。这意味着你需要显示地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
创建类型实例
泛型数组
4 边界(bound)
边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。
因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。
但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。
通配符被限制为单一边界。
5 通配符(wildcards)
数组的一种特殊行为
可以将子类型的数组赋给基类型的数组引用。然后编译期数组元素可以放置基类型及其子类型的元素,即编译时不报错,但运行时的数组机制知道实际的数组类型是子类,因此会在运行时检查放置的类型是否是实际类型及其再导出的子类型,不是则抛出java.lang.ArrayStoreException
异常。容器的类型与容器持有的类型
// Compile Error: incompatible types:List<Fruit> list = new ArrayList<Apple>();
与数组不同,泛型没有内建的协变类型。即*协变性对泛型不起作用。
package net.mrliuli.generics.wildcards;import java.util.*;/** * Created by leon on 2017/12/8. */public class GenericsAndCovariance { public static void main(String[] args){ // Compile Error: incompatible types: //List<Fruit> list = new ArrayList<Apple>(); // Wildcards allow covariance: List<? extends Fruit> flists = new ArrayList<Apple>(); // But, 编译器并不知道flists持有什么类型对象。实际上上面语句使得向上转型,丢失掉了向List中传递任何对象的能力,甚至是传递Object也不行。 //flists.add(new Apple()); //flists.add(new Fruit()); //flists.add(new Object()); flists.add(null); // legal but uninteresting // We know that it returns at least Fruit: Fruit f = flists.get(0); } }
5.1 编译器有多聪明
对于
List<? extends Fruit>
,set()
方法不能工作于Apple
和Fruit
,因为 set() 的参数也是? extends Furit
,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。但是,
equals()
方法工作良好,因为它将接受Object类型而并非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。
5.2 逆变(Contravariance)
使用超类型通配符。声明通配符是由某个特定类的任何基类界定的,方法是指定
<? super MyClass>
,甚至或者使用类型参数:<? super T>
。这使得你可以安全地传递一个类型对象到泛型类型中。参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。
package net.mrliuli.generics.wildcards;import java.util.*;public class SuperTypeWildcards { /** * 超类型通配符使得可以向泛型容器写入。超类型边界放松了在可以向方法传递的参数上所作的限制。 * @param apples 参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。 */ static void writeTo(List<? super Apple> apples){ apples.add(new Apple()); apples.add(new Jonathan()); //apples.add(new Fruit()); // Error } }
GenericWriting.java 中
writeExact(fruitList, new Apple());
在JDK1.7中没有报错,说明进入泛型方法writeExact()
时T
被识别为Fruit
,书中说报错,可能JDK1.5将T
识别为Apple
。
package net.mrliuli.generics.wildcards;import java.util.*;/** * Created by li.liu on 2017/12/8. */public class GenericWriting { static <T> void writeExact(List<T> list, T item){ list.add(item); } static List<Apple> appleList = new ArrayList<Apple>(); static List<Fruit> fruitList = new ArrayList<Fruit>(); static void f1(){ writeExact(appleList, new Apple()); writeExact(fruitList, new Apple()); } static <T> void writeWithWildcard(List<? super T> list, T item){ list.add(item); } static void f2(){ writeWithWildcard(appleList, new Apple()); writeWithWildcard(fruitList, new Apple()); } public static void main(String[] args){ f1(); f2(); } }
5.3 无界通配符(Unbounded wildcards)
原生泛型Holder
与Holder<?>
原生
Holder
将持有任何类型的组合,而Holder<?>
将持有具有某种具体类型的同构集合,因此不能只是向其中传递Object。
5.4 捕获转换
以下示例,被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。参数类型在调用
f2()
的过程中被捕获,因此它可以在对f1()
的调用中被使用。
package net.mrliuli.generics.wildcards;/** * Created by leon on 2017/12/9. */public class CaptureConversion { static <T> void f1(Holder<T> holder){ T t = holder.get(); System.out.println(t.getClass().getSimpleName()); } static void f2(Holder<?> holder){ f1(holder); // Call with captured type } public static void main(String[] args){ Holder raw = new Holder<Integer>(1); f1(raw); f2(raw); Holder rawBasic = new Holder(); rawBasic.set(new Object()); f2(rawBasic); Holder<?> wildcarded = new Holder<Double>(1.0); f2(wildcarded); } }
6 问题
基本类型不能作为类型参数
由于探险,一个类不能实现同一个泛型接口的两种变体
由于擦除,通过泛型来重载方法将产生相同的签名,编译出错,不能实现重载
基类劫持了接口
7 总结
我相信被称为泛型的通用语言特性(并非必须是其在Java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。
泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型有更少的限制,因此单个的代码段能够应用到更多的类型上。
相关文章:
위 내용은 자바 프로그래밍 사고 학습 수업(3) 15장 - 제네릭의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











Java의 난수 생성기 안내. 여기서는 예제를 통해 Java의 함수와 예제를 통해 두 가지 다른 생성기에 대해 설명합니다.

Java의 Weka 가이드. 여기에서는 소개, weka java 사용 방법, 플랫폼 유형 및 장점을 예제와 함께 설명합니다.

Java의 Smith Number 가이드. 여기서는 정의, Java에서 스미스 번호를 확인하는 방법에 대해 논의합니다. 코드 구현의 예.

이 기사에서는 가장 많이 묻는 Java Spring 면접 질문과 자세한 답변을 보관했습니다. 그래야 면접에 합격할 수 있습니다.

Java 8은 스트림 API를 소개하여 데이터 컬렉션을 처리하는 강력하고 표현적인 방법을 제공합니다. 그러나 스트림을 사용할 때 일반적인 질문은 다음과 같은 것입니다. 기존 루프는 조기 중단 또는 반환을 허용하지만 스트림의 Foreach 메소드는이 방법을 직접 지원하지 않습니다. 이 기사는 이유를 설명하고 스트림 처리 시스템에서 조기 종료를 구현하기위한 대체 방법을 탐색합니다. 추가 읽기 : Java Stream API 개선 스트림 foreach를 이해하십시오 Foreach 메소드는 스트림의 각 요소에서 하나의 작업을 수행하는 터미널 작동입니다. 디자인 의도입니다

Java의 TimeStamp to Date 안내. 여기서는 소개와 예제와 함께 Java에서 타임스탬프를 날짜로 변환하는 방법에 대해서도 설명합니다.

Java는 초보자와 숙련된 개발자 모두가 배울 수 있는 인기 있는 프로그래밍 언어입니다. 이 튜토리얼은 기본 개념부터 시작하여 고급 주제를 통해 진행됩니다. Java Development Kit를 설치한 후 간단한 "Hello, World!" 프로그램을 작성하여 프로그래밍을 연습할 수 있습니다. 코드를 이해한 후 명령 프롬프트를 사용하여 프로그램을 컴파일하고 실행하면 "Hello, World!"가 콘솔에 출력됩니다. Java를 배우면 프로그래밍 여정이 시작되고, 숙달이 깊어짐에 따라 더 복잡한 애플리케이션을 만들 수 있습니다.
