JavaRush /Java 博客 /Random-ZH /Java 中的泛型理论或如何在实践中使用括号
Viacheslav
第 3 级

Java 中的泛型理论或如何在实践中使用括号

已在 Random-ZH 群组中发布

介绍

从 JSE 5.0 开始,泛型被添加到 Java 语言库中。
Java 中的泛型理论或如何在实践中使用括号 - 1

Java 中的泛型是什么?

泛型(泛化)是 Java 语言用于实现泛化编程的特殊方法:一种描述数据和算法的特殊方法,允许您在不更改其描述的情况下处理不同类型的数据。在 Oracle 网站上,有一个专门针对泛型的单独教程:“课程:泛型”。

首先,要了解泛型,您需要了解为什么需要它们以及它们提供什么。在教程的“为什么使用泛型?”部分中 据说目的之一是更强的编译时类型检查并消除显式转换的需要。
Java 中的泛型理论或如何在实践中使用括号 - 2
让我们准备我们最喜欢的教程点在线java编译器进行实验。让我们想象一下这段代码:
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,因为 这是整数。他会这样告诉我们。许多人将泛型称为“语法糖”。他们是对的,因为泛型在编译时确实会变成相同的种姓。让我们看看编译后的类的字节码:使用手动转换和使用泛型:
Java 中的泛型理论或如何在实践中使用括号 - 3
编译后,有关泛型的任何信息都将被删除。这称为“类型擦除”或“类型擦除”。类型擦除和泛型旨在提供与旧版本 JDK 的向后兼容性,同时仍然允许编译器协助新版本 Java 中的类型推断。
Java 中的泛型理论或如何在实践中使用括号 - 4

原始类型或原始类型

当谈论泛型时,我们总是有两类:类型化类型(泛型类型)和“原始”类型(原始类型)。 原始类型是没有在尖括号中指定“限定”的类型:
Java 中的泛型理论或如何在实践中使用括号 - 5
类型化类型则相反,带有“澄清”的指示:
Java 中的泛型理论或如何在实践中使用括号 - 6
正如我们所看到的,我们使用了一种不寻常的设计,在屏幕截图中用箭头标记。这是Java SE 7中添加的一种特殊语法,它被称为“ the Diamond ”,意思是钻石。为什么?您可以将菱形的形状与大括号的形状进行类比:菱形语法还与“类型推断”或类型推断<> 的概念相关联。毕竟,编译器在看到右侧的 <> 时,会查看左侧,即赋值的变量类型的声明所在的位置。从这部分他就明白了右边的值是什么类型。事实上,如果在左侧指定了泛型而不在右侧指定,编译器将能够推断出类型:
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 翻译为抑制,即字面意思是抑制警告。但想想你为什么决定表明这一点?记住第一条规则,也许您需要添加打字。
Java 中的泛型理论或如何在实践中使用括号 - 7

通用方法

泛型允许您键入方法。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,并且代码执行失败并出现错误。
Java 中的泛型理论或如何在实践中使用括号 - 8

通用类型

您不仅可以键入方法,还可以键入类本身。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原则适用于通配符。它们可以用以下形式表示:
Java 中的泛型理论或如何在实践中使用括号 - 9
该原则也称为PECS(生产者扩展消费者超级)原则。您可以在文章“使用泛型通配符提高 Java API 的可用性”以及 stackoverflow 上的精彩讨论中阅读有关 Habré 的更多信息:“在泛型 Java 中使用通配符”。下面是来自 Java 源代码的一个小示例 - Collections.copy 方法:
Java 中的泛型理论或如何在实践中使用括号 - 10
好吧,举一个小例子来说明它如何不起作用:
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>();
因为继承与泛型的工作方式不同:
Java 中的泛型理论或如何在实践中使用括号 - 11
这是另一个会因错误而失败的好例子:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
这里的一切也很简单。尽管 String 是 Object 的后代,但 List<String> 不是 List<Object> 的后代。

最终的

所以我们刷新了对泛型的记忆。如果很少充分利用它们的全部力量,一些细节就会被遗忘。我希望这篇简短的评论能帮助您刷新记忆。为了获得更好的结果,我强烈建议您阅读以下材料: #维亚切斯拉夫
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION