JavaRush /Java 博客 /Random-ZH /猫的仿制药
Viacheslav
第 3 级

猫的仿制药

已在 Random-ZH 群组中发布
猫的仿制药 - 1

介绍

今天是记住我们对 Java 的了解的好日子。根据最重要的文件,即 Java 语言规范(JLS - Java Language Specifiaction),Java 是一种强类型语言,如“第 4 章。类型、值和变量”一章中所述。这是什么意思?假设我们有一个 main 方法:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
强类型确保编译此代码时,编译器将检查是否将文本变量的类型指定为 String,然后我们不会尝试在任何地方将其用作其他类型的变量(例如,作为 Integer) 。例如,如果我们尝试保存一个值而不是文本2L(即 long 而不是 String),我们将在编译时收到错误:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
那些。强类型允许您确保仅当这些操作对于这些对象合法时才执行对对象的操作。这也称为类型安全。正如 JLS 中所述,Java 中有两类类型:原始类型和引用类型。您可以记得评论文章中有关原始类型的内容:“ Java 中的原始类型:它们并不那么原始。” 引用类型可以由类、接口或数组表示。今天我们将对引用类型感兴趣。让我们从数组开始:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
这段代码运行没有错误。我们知道(例如,来自“ Oracle Java 教程:数组”),数组是仅存储一种类型数据的容器。在这种情况下 - 只有线条。让我们尝试向数组添加 long 而不是 String:
text[1] = 4L;
让我们运行这段代码(例如,在Repl.it Online Java Compiler中)并得到一个错误:
error: incompatible types: long cannot be converted to String
该语言的数组和类型安全不允许我们将不适合该类型的内容保存到数组中。这是类型安全的体现。我们被告知:“修复错误,但在那之前我不会编译代码。” 最重要的是,这发生在编译时,而不是程序启动时。也就是说,我们会立即看到错误,而不是“有一天”。既然我们记住了数组,那么让我们也记住Java 集合框架。我们在那里有不同的结构。例如,列表。让我们重写这个例子:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
编译时,我们会test在变量初始化行收到错误:
incompatible types: Object cannot be converted to String
在我们的例子中,List 可以存储任何对象(即 Object 类型的对象)。因此,编译器表示不能承担这样的责任。因此,我们需要显式指定从列表中获取的类型:
String test = (String) text.get(0);
这种指示称为类型转换或类型转换。现在一切都会正常工作,直到我们尝试获取索引 1 处的元素,因为 它是 Long 类型。我们会得到一个公平的错误,但已经在程序运行时(在运行时):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
正如我们所看到的,这里有几个重要的缺点。首先,我们被迫将从列表中获得的值“转换”到 String 类。同意,这很丑。其次,如果出现错误,我们只有在程序执行时才会看到它。如果我们的代码更复杂,我们可能不会立即检测到此类错误。而开发人员开始思考如何让这种情况下的工作变得更轻松,让代码更清晰。他们就这样诞生了——泛型。
猫的仿制药 - 2

泛型

所以,泛型。它是什么?泛型是描述所使用类型的特殊方式,代码编译器可以在其工作中使用它来确保类型安全。它看起来像这样:
猫的仿制药 - 3
这是一个简短的示例和解释:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
在这个例子中,我们说我们不仅有List,还有List,它只适用于 String 类型的对象。没有其他人。括号里的内容就是我们可以存储的。这样的“括号”称为“尖括号”,即 尖括号。编译器会友好地检查我们在处理字符串列表(该列表被命名为文本)时是否犯了任何错误。编译器会发现我们正在厚颜无耻地尝试将 Long 放入 String 列表中。并且在编译的时候会报错:
error: no suitable method found for add(long)
您可能还记得 String 是 CharSequence 的后代。并决定做类似的事情:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
但这是不可能的,我们会得到错误: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> 这看起来很奇怪,因为。该行CharSequence sec = "test";不包含错误。让我们弄清楚一下。他们谈到这种行为时说:“泛型是不变的。” 什么是“不变量”?我喜欢维基百科上“协变和逆变”一文中对此的说法:
猫的仿制药 - 4
因此,不变性是指派生类型之间不存在继承。如果 Cat 是 Animals 的子类型,则 Set<Cats> 不是 Set<Animals> 的子类型,并且 Set<Animals> 也不是 Set<Cats> 的子类型。顺便说一下,值得一提的是,从 Java SE 7 开始,出现了所谓的“钻石操作符”。因为两个尖括号<>就像一个菱形。这允许我们使用像这样的泛型:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
根据这段代码,编译器可以理解,如果我们在左侧表示它将List包含 String 类型的对象,那么在右侧我们的意思是我们要将lines一个新的 ArrayList 保存到一个变量中,该变量也将存储一个对象左侧指定的类型。因此,左侧的编译器可以理解或推断右侧的类型。这就是为什么这种行为在英语中被称为类型推断或“类型推断”。另一个值得注意的有趣的事情是 RAW 类型或“原始类型”。因为 泛型并不总是存在,Java 会尽可能地保持向后兼容性,然后泛型被迫以某种方式处理未指定泛型的代码。让我们看一个例子:
List<CharSequence> lines = new ArrayList<String>();
我们记得,由于泛型的不变性,这样的行将无法编译。
List<Object> lines = new ArrayList<String>();
出于同样的原因,这个也不会编译。
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
这些行将编译并运行。其中使用了原始类型,即 未指定类型。再次值得指出的是,原始类型不应该在现代代码中使用。
猫的仿制药 - 5

类型化类

所以,输入类。让我们看看如何编写我们自己的类型化类。例如,我们有一个类层次结构:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
我们想要创建一个实现动物容器的类。可以编写一个包含任何Animal. 这很简单,可以理解,但是……狗和猫混在一起是不好的,它们不是彼此的朋友。此外,如果有人收到这样的容器,他可能会错误地将猫从容器中扔进一群狗中..​​....这不会带来任何好处。在这里,泛型将帮助我们。例如,让我们这样编写实现:
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
我们的类将使用由名为 T 的泛型指定的类型的对象。这是一种别名。因为 泛型是在类名中指定的,那么我们在声明类时就会收到它:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
正如我们所看到的,我们表明我们有Box,它仅适用于Cat。编译器意识到,您需要在指定泛型名称的catBox地方T替换类型,而不是泛型: CatT
猫的仿制药 - 6
那些。多亏了Box<Cat>编译器,它才明白slots它实际上应该是什么List<Cat>。因为Box<Dog>里面会有slots,包含List<Dog>。类型声明中可以有多个泛型,例如:
public static class Box<T, V> {
泛型的名称可以是任何东西,尽管建议遵守一些潜规则 - “类型参数命名约定”:元素类型 - E,键类型 - K,数字类型 - N,T - for type,V - for值类型。顺便说一下,请记住我们说过泛型是不变的,即 不保留继承层次结构。事实上,我们可以影响这一点。也就是说,我们有机会使泛型变得协变,即 以相同的顺序保持继承。这种行为称为“有界类型”,即 种类有限。例如,我们的类Box可以包含所有动物,那么我们将声明一个像这样的泛型:
public static class Box<T extends Animal> {
也就是我们给class设置了上限Animal。我们还可以在关键字后面指定几种类型extends。这意味着我们将使用的类型必须是某个类的后代,并且同时实现某个接口。例如:
public static class Box<T extends Animal & Comparable> {
在这种情况下,如果我们尝试将Box一些内容放入非继承者Animal且未实现的内容中Comparable,那么在编译过程中我们将收到错误:
error: type argument Cat is not within bounds of type-variable T
猫的仿制药 - 7

方法输入

泛型不仅用在类型中,还用在单个方法中。方法的应用可以参见官方教程:《泛型方法》。

背景:

猫的仿制药 - 8
让我们看看这张照片。正如您所看到的,编译器查看方法签名并发现我们正在将一些未定义的类作为输入。它并不能通过签名确定我们正在返回某种对象,即 目的。因此,如果我们想创建一个 ArrayList,那么我们需要这样做:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
你必须明确地写出输出将是一个 ArrayList,这很丑陋并且增加了出错的机会。例如,我们可以写这样的废话,它会编译:
ArrayList object = (ArrayList) createObject(LinkedList.class);
我们可以帮助编译器吗?是的,泛型允许我们做到这一点。让我们看一下同一个例子:
猫的仿制药 - 9
然后,我们可以像这样简单地创建一个对象:
ArrayList<String> object = createObject(ArrayList.class);
猫的仿制药 - 10

通配符

根据Oracle的泛型教程,特别是“通配符”部分,我们可以用问号来描述“未知类型”。通配符是一个方便的工具,可以减轻泛型的一些限制。例如,正如我们之前讨论的,泛型是不变的。这意味着虽然所有类都是 Object 类型的后代(子类型),但它List<любой тип>不是子类型List<Object>。但是,List<любой тип>它是一个子类型List<?>。所以我们可以编写如下代码:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
与常规泛型(即不使用通配符)一样,带有通配符的泛型也可能受到限制。上界通配符看起来很熟悉:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
但您也可以通过下界通配符来限制它:
public static void printCatList(List<? super Cat> list) {
因此,该方法将开始接受所有猫,以及层次结构中更高的猫(直到对象)。
猫的仿制药 - 11

类型擦除

说到泛型,值得了解一下“类型擦除”。事实上,类型擦除是关于泛型是编译器的信息这一事实。在程序执行过程中,不再有关于泛型的信息,这称为“擦除”。此擦除的效果是泛型类型被特定类型替换。如果泛型没有边界,则将替换对象类型。如果指定了边框(例如<T extends Comparable>),则它将被替换。以下是 Oracle 教程中的一个示例:“通用类型的擦除”:
猫的仿制药 - 12
如上所述,在这个例子中,泛型T被擦除到其边界,即 前Comparable
猫的仿制药 - 13

结论

泛型是一个非常有趣的话题。我希望您对这个话题感兴趣。总而言之,泛型是一个很好的工具,开发人员可以向编译器提供附加信息,一方面确保类型安全,另一方面确保灵活性。如果您有兴趣,那么我建议您查看我喜欢的资源: #维亚切斯拉夫
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION