介绍
从 JSE 5.0 开始,泛型被添加到 Java 语言库中。Java 中的泛型是什么?
泛型(泛化)是 Java 语言用于实现泛化编程的特殊方法:一种描述数据和算法的特殊方法,允许您在不更改其描述的情况下处理不同类型的数据。在 Oracle 网站上,有一个专门针对泛型的单独教程:“课程:泛型”。
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
这段代码将运行良好。但如果他们来找我们并说“你好,世界!”怎么办?被打了只能回你啰?让我们从代码中删除与字符串的连接", world!"
。看来还有什么比这更无害的呢?但事实上,我们会在编译期间收到一个错误: error: incompatible types: Object cannot be converted to String
在我们的例子中,List 存储了一个 Object 类型的对象列表。由于 String 是 Object 的后代(因为 Java 中所有类都是从 Object 隐式继承的),因此它需要显式强制转换,但我们没有这样做。并且在连接时,将在该对象上调用静态方法 String.valueOf(obj) ,该方法最终将调用该对象上的 toString 方法。也就是说,我们的List包含Object。事实证明,当我们需要特定类型而不是 Object 时,我们必须自己进行类型转换:
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println((String)str);
}
}
}
然而,在这种情况下,因为 List接受对象列表,它不仅存储String,还存储Integer。但最糟糕的是,在这种情况下编译器不会发现任何错误。在这里,我们将在执行期间收到一个错误(他们还说错误是“在运行时”收到的)。错误将是:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
同意,但不是最令人愉快的。而这一切都是因为编译器不是人工智能,它无法猜测程序员的所有意思。为了告诉编译器更多关于我们将使用什么类型的信息,Java SE 5 引入了泛型。让我们通过告诉编译器我们想要什么来纠正我们的版本:
import java.util.*;
public class HelloWorld {
public static void main(String []args){
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println(str);
}
}
}
正如我们所看到的,我们不再需要转换为 String。此外,我们现在有了用来框定泛型的尖括号。现在,编译器将不允许编译该类,直到我们删除列表中添加的 123,因为 这是整数。他会这样告诉我们。许多人将泛型称为“语法糖”。他们是对的,因为泛型在编译时确实会变成相同的种姓。让我们看看编译后的类的字节码:使用手动转换和使用泛型:
原始类型或原始类型
当谈论泛型时,我们总是有两类:类型化类型(泛型类型)和“原始”类型(原始类型)。 原始类型是没有在尖括号中指定“限定”的类型:<>
的概念相关联。毕竟,编译器在看到右侧的 <> 时,会查看左侧,即赋值的变量类型的声明所在的位置。从这部分他就明白了右边的值是什么类型。事实上,如果在左侧指定了泛型而不在右侧指定,编译器将能够推断出类型:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello World");
String data = list.get(0);
System.out.println(data);
}
}
然而,这将是带有泛型的新样式和不带泛型的旧样式的混合。这是极其不可取的。编译上面的代码时,我们将收到消息:Note: HelloWorld.java uses unchecked or unsafe operations
。事实上,似乎根本不清楚为什么需要在这里添加钻石。但这里有一个例子:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
我们记得,ArrayList 还有第二个构造函数,它将集合作为输入。而这正是欺骗之所在。如果没有钻石语法,编译器不会明白它被欺骗了,但有了钻石语法,编译器就会明白。因此,规则#1:如果我们使用类型化类型,则始终使用菱形语法。否则,我们可能会在使用原始类型的地方丢失数据。为了避免日志中出现“使用未经检查或不安全的操作”的警告,可以在所使用的方法或类上指定特殊的注释:@SuppressWarnings("unchecked")
Suppress 翻译为抑制,即字面意思是抑制警告。但想想你为什么决定表明这一点?记住第一条规则,也许您需要添加打字。
通用方法
泛型允许您键入方法。Oracle 教程中有一个单独的部分专门介绍此功能:“通用方法”。在本教程中,记住语法很重要:- 包括尖括号内的类型参数列表;
- 类型化参数列表位于返回的方法之前。
import java.util.*;
public class HelloWorld{
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
如果您查看 Util 类,我们会在其中看到两个类型化方法。通过类型推断,我们可以直接向编译器提供类型定义,也可以自己指定。示例中提供了这两个选项。顺便说一句,如果你仔细想想,语法是非常合乎逻辑的。当键入一个方法时,我们在方法之前指定泛型,因为如果我们在方法之后使用泛型,Java 将无法确定要使用哪种类型。因此,我们首先宣布我们将使用泛型 T,然后我们说我们将返回这个泛型。当然,Util.<Integer>getValue(element, String.class)
它会因错误而失败incompatible types: Class<String> cannot be converted to Class<Integer>
。使用类型化方法时,您应该始终记住类型擦除。让我们看一个例子:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
效果会很好。但前提是编译器知道被调用的方法具有 Integer 类型。让我们用以下行替换控制台输出: System.out.println(Util.getValue(element) + 1);
我们得到错误:二元运算符 '+' 的错误操作数类型,第一个类型: Object ,第二个类型: int 也就是说,类型已被删除。编译器发现没有人指定类型,类型被指定为 Object,并且代码执行失败并出现错误。
通用类型
您不仅可以键入方法,还可以键入类本身。Oracle 在其指南中专门有一个“通用类型”部分。让我们看一个例子:public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
这里一切都很简单。如果我们使用类,则泛型会列在类名后面。现在让我们在 main 方法中创建此类的实例:
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
效果会很好。编译器看到有一个数字列表和一个字符串类型的集合。但是如果我们删除泛型并执行以下操作会怎样:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
我们会得到错误:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
再次键入擦除。由于该类不再具有泛型,因此编译器认为我们传递了一个 List,因此使用 List<Integer> 的方法更合适。我们犯了一个错误。因此,规则#2:如果类是类型化的,则始终在 generic 中指定类型。
限制
我们可以对泛型中指定的类型应用限制。例如,我们希望容器仅接受 Number 作为输入。Oracle 教程的“有界类型参数”部分描述了此功能。让我们看一个例子:import java.util.*;
public class HelloWorld{
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
正如您所看到的,我们将泛型类型限制为 Number 类/接口及其后代。有趣的是,您不仅可以指定类,还可以指定接口。例如: public static class NumberContainer<T extends Number & Comparable> {
泛型还有通配符的概念 https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html 它们又分为三种:
所谓的Get Put原则适用于通配符。它们可以用以下形式表示:
public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
但如果你把extends换成super,一切都会好起来的。由于我们在输出之前先给列表填充了一个值,所以它对于我们来说就是一个消费者,也就是一个消费者。因此,我们使用super。
遗产
泛型还有另一个不寻常的特性——它们的继承性。Oracle 教程的“泛型、继承和子类型”部分描述了泛型的继承。最主要的是记住并认识到以下几点。我们不能这样做:List<CharSequence> list1 = new ArrayList<String>();
因为继承与泛型的工作方式不同:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
这里的一切也很简单。尽管 String 是 Object 的后代,但 List<String> 不是 List<Object> 的后代。
GO TO FULL VERSION