字符和字符串总结
原文:
docs.oracle.com/javase/tutorial/java/data/stringsummary.html
大多数情况下,如果您使用单个字符值,您将使用基本的char
类型。然而,有时您需要将 char 用作对象—例如,作为期望对象的方法参数。Java 编程语言为此提供了一个包装类,将char
包装在Character
对象中。Character
类型的对象包含一个类型为char
的单个字段。这个Character
类还提供了许多有用的类(即静态)方法来操作字符。
字符串是字符序列,在 Java 编程中被广泛使用。在 Java 编程语言中,字符串是对象。String
类有 60 多个方法和 13 个构造函数。
最常见的是,您可以使用类似于以下语句创建一个字符串
String s = "Hello world!";
而不是使用其中一个String
构造函数。
String
类有许多方法可以查找和检索子字符串;然后可以使用+
连接运算符将它们轻松重新组装成新的字符串。
String
类还包括许多实用方法,其中包括split()
、toLowerCase()
、toUpperCase()
和valueOf()
。后者方法在将用户输入的字符串转换为数字时是不可或缺的。Number
子类还有将字符串转换为数字以及反之的方法。
除了String
类之外,还有一个StringBuilder
类。与字符串一起工作相比,使用StringBuilder
对象有时可能更有效率。StringBuilder
类提供了一些对字符串有用的方法,其中包括reverse()
。然而,总的来说,String
类具有更广泛的方法。
可以使用StringBuilder
构造函数将字符串转换为字符串构建器。可以使用toString()
方法将字符串构建器转换为字符串。
自动装箱和拆箱
原文:
docs.oracle.com/javase/tutorial/java/data/autoboxing.html
自动装箱是 Java 编译器在原始类型和其对应的对象包装类之间进行的自动转换。例如,将int
转换为Integer
,将double
转换为Double
等。如果转换反向进行,则称为拆箱。
这是自动装箱的最简单示例:
Character ch = 'a';
本节中的其余示例使用泛型。如果您还不熟悉泛型的语法,请参阅泛型(更新)课程。
考虑以下代码:
List<Integer> li = new ArrayList<>(); for (int i = 1; i < 50; i += 2) li.add(i);
尽管您将int
值作为原始类型而不是Integer
对象添加到li
中,但代码仍然可以编译。因为li
是Integer
对象的列表,而不是int
值的列表,您可能会想知道为什么 Java 编译器没有发出编译时错误。编译器不会生成错误,因为它从i
创建一个Integer
对象并将该对象添加到li
中。因此,编译器在运行时将前面的代码转换为以下代码:
List<Integer> li = new ArrayList<>(); for (int i = 1; i < 50; i += 2) li.add(Integer.valueOf(i));
将原始值(例如int
)转换为相应包装类(Integer
)的对象称为自动装箱。当原始值是以下情况时,Java 编译器会应用自动装箱:
- 作为传递给期望相应包装类对象的方法的参数。
- 赋给相应包装类的变量。
考虑以下方法:
public static int sumEven(List<Integer> li) { int sum = 0; for (Integer i: li) if (i % 2 == 0) sum += i; return sum; }
因为余数(%
)和一元加号(+=
)运算符不适用于Integer
对象,您可能会想知道为什么 Java 编译器在不发出任何错误的情况下编译该方法。编译器不会生成错误,因为它在运行时调用intValue
方法将Integer
转换为int
:
public static int sumEven(List<Integer> li) { int sum = 0; for (Integer i : li) if (i.intValue() % 2 == 0) sum += i.intValue(); return sum; }
将包装类型的对象(Integer
)转换为其对应的原始类型(int
)值称为拆箱。当包装类的对象是以下情况时,Java 编译器会应用拆箱:
- 作为传递给期望相应原始类型值的方法的参数。
- 赋给相应原始类型的变量。
Unboxing
示例展示了这是如何工作的:
import java.util.ArrayList; import java.util.List; public class Unboxing { public static void main(String[] args) { Integer i = new Integer(-8); // 1\. Unboxing through method invocation int absVal = absoluteValue(i); System.out.println("absolute value of " + i + " = " + absVal); List<Double> ld = new ArrayList<>(); ld.add(3.1416); // Π is autoboxed through method invocation. // 2\. Unboxing through assignment double pi = ld.get(0); System.out.println("pi = " + pi); } public static int absoluteValue(int i) { return (i < 0) ? -i : i; } }
该程序打印如下内容:
absolute value of -8 = 8 pi = 3.1416
自动装箱和拆箱使开发人员编写更清晰的代码,使其更易于阅读。以下表列出了原始类型及其对应的包装类,这些包装类由 Java 编译器用于自动装箱和拆箱:
Primitive type | Wrapper class |
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
问题和练习:字符和字符串
原文:
docs.oracle.com/javase/tutorial/java/data/QandE/characters-questions.html
问题
- 以下字符串构建器的初始容量是多少?
StringBuilder sb = new StringBuilder("Able was I ere I saw Elba.");
- 考虑以下字符串:
String hannah = "Did Hannah see bees? Hannah did.";
- 表达式
hannah.length()
显示的值是多少? - 方法调用
hannah.charAt(12)
返回的值是多少? - 编写一个表达式,引用
hannah
所指的字符串中的字母b
。
- 以下表达式返回的字符串有多长?这个字符串是什么?
"Was it a car or a cat I saw?".substring(9, 12)
- 在下面的程序中,名为
ComputeResult
,每个编号行执行后result
的值是多少?
public class ComputeResult { public static void main(String[] args) { String original = "software"; StringBuilder result = new StringBuilder("hi"); int index = original.indexOf('a'); /*1*/ result.setCharAt(0, original.charAt(0)); /*2*/ result.setCharAt(1, original.charAt(original.length()-1)); /*3*/ result.insert(1, original.charAt(4)); /*4*/ result.append(original.substring(1,4)); /*5*/ result.insert(3, (original.substring(index, index+2) + " ")); System.out.println(result); } }
练习
- 展示两种方法将以下两个字符串连接在一起以得到字符串
"Hi, mom."
:
String hi = "Hi, "; String mom = "mom.";
- 编写一个程序,从你的全名中计算出你的缩写并显示出来。
- 一个变位词是由另一个单词或短语的字母重新排列而成的单词或短语;例如,“parliament”是“partial men”的变位词,“software”是“swear oft”的变位词。编写一个程序,判断一个字符串是否是另一个字符串的变位词。该程序应忽略空格和标点符号。
检查你的答案。
Lesson: 泛型(更新)
在任何非平凡的软件项目中,错误都是生活中不可避免的事实。仔细的规划、编程和测试可以帮助减少它们的普遍性,但不知何故,它们总会找到一种方式悄悄地潜入你的代码中。随着新功能的引入和代码库规模与复杂性的增长,这一点变得尤为明显。
幸运的是,有些错误比其他错误更容易检测。例如,编译时错误可以在早期被检测出来;你可以利用编译器的错误消息来找出问题所在并立即修复它。然而,运行时错误可能会更加棘手;它们并不总是立即显现,而且当它们出现时,可能是在程序中与问题实际原因相距甚远的地方。
泛型通过在编译时使更多的错误可检测,为你的代码增加了稳定性。完成本课程后,你可能想继续学习 Gilad Bracha 的《泛型》教程。
为什么使用泛型?
简而言之,泛型使类型(类和接口)在定义类、接口和方法时成为参数。就像在方法声明中使用的更熟悉的形式参数一样,类型参数提供了一种方式让您可以重复使用相同的代码以不同的输入。不同之处在于,形式参数的输入是值,而类型参数的输入是类型。
使用泛型的代码比不使用泛型的代码有许多好处:
- 编译时进行更强的类型检查。
Java 编译器对泛型代码应用强类型检查,如果代码违反类型安全性,则会发出错误。修复编译时错误比修复运行时错误更容易,后者可能很难找到。 - 消除强制类型转换。
不使用泛型的以下代码片段需要进行强制类型转换:
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0);
- 当重写为使用泛型时,代码不需要进行强制类型转换:
List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast
- 使程序员能够实现泛型算法。
通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,可以进行定制,并且是类型安全且更易阅读的。
泛型类型
泛型类型是一个参数化类型的泛型类或接口。下面的 Box
类将被修改以演示这个概念。
一个简单的 Box 类
首先看一下一个操作任意类型对象的非泛型 Box
类。它只需要提供两个方法:set
,用于向盒子中添加对象,和 get
,用于检索对象:
public class Box { private Object object; public void set(Object object) { this.object = object; } public Object get() { return object; } }
由于它的方法接受或返回一个 Object
,你可以自由传入任何你想要的东西,只要不是原始类型之一。在编译时无法验证类的使用方式。代码的一部分可能将一个 Integer
放入盒子中,并期望从中获取 Integer
,而代码的另一部分可能错误地传入一个 String
,导致运行时错误。
Box 类的泛型版本
一个泛型类的定义格式如下:
class name<T1, T2, ..., Tn> { /* ... */ }
类型参数部分,由尖括号(<>
)界定,跟在类名后面。它指定了类型参数(也称为类型变量)T1
、T2
、… 和 Tn
。
要将 Box
类更新为使用泛型,你需要通过将代码 “public class Box
” 更改为 “public class Box
” 来创建一个泛型类型声明。这引入了类型变量 T
,可以在类内部的任何地方使用。
有了这个改变,Box
类变成了:
/** * Generic version of the Box class. * @param <T> the type of the value being boxed */ public class Box<T> { // T stands for "Type" private T t; public void set(T t) { this.t = t; } public T get() { return t; } }
正如你所看到的,所有 Object
的出现都被 T
替换。类型变量可以是你指定的任何非原始类型:任何类类型、任何接口类型、任何数组类型,甚至是另一个类型变量。
这种技术也可以应用于创建泛型接口。
类型参数命名约定
按照惯例,类型参数的名称是单个大写字母。这与你已经了解的变量命名约定形成鲜明对比,而且有充分的理由:没有这个约定,很难区分类型变量和普通类或接口名称。
最常用的类型参数名称有:
- E - Element(Java 集合框架广泛使用)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V 等 - 第二、第三、第四种类型
你将在 Java SE API 和本课程的其余部分中看到这些名称的使用。
调用和实例化泛型类型
要在代码中引用泛型 Box
类,你必须执行一个泛型类型调用,将 T
替换为某个具体值,比如 Integer
:
Box<Integer> integerBox;
你可以将泛型类型调用看作类似于普通方法调用,但是不是向方法传递参数,而是向 Box
类本身传递一个类型参数 — 在本例中是 Integer
。
类型参数和类型参数术语: 许多开发人员将“类型参数”和“类型参数”这两个术语互换使用,但这两个术语并不相同。在编码时,为了创建参数化类型,需要提供类型参数。因此,在Foo
中,T
是类型参数,而在Foo f
中的String
是类型参数。本课程在使用这些术语时遵守此定义。
与任何其他变量声明一样,此代码实际上并不创建新的Box
对象。它只是声明integerBox
将保存对“Box
of Integer
”的引用,这就是Box
的含义。
通常将泛型类型的调用称为参数化类型。
要实例化此类,像往常一样使用new
关键字,但是在类名和括号之间放置:
Box<Integer> integerBox = new Box<Integer>();
钻石
在 Java SE 7 及更高版本中,只要编译器可以从上下文中确定或推断出类型参数,就可以用空类型参数集(<>)替换调用泛型类构造函数所需的类型参数。这一对尖括号<>非正式地称为钻石。例如,您可以使用以下语句创建Box
的实例:
Box<Integer> integerBox = new Box<>();
有关钻石符号和类型推断的更多信息,请参见类型推断。
多个类型参数
如前所述,泛型类可以具有多个类型参数。例如,实现泛型Pair
接口的泛型OrderedPair
类:
public interface Pair<K, V> { public K getKey(); public V getValue(); } public class OrderedPair<K, V> implements Pair<K, V> { private K key; private V value; public OrderedPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
以下语句创建了OrderedPair
类的两个实例:
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8); Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
代码new OrderedPair
实例化K
为String
,V
为Integer
。因此,OrderedPair
的构造函数的参数类型分别为String
和Integer
。由于自动装箱,将String
和int
传递给类是有效的。
如钻石中所述,因为 Java 编译器可以从声明OrderedPair
中推断出K
和V
类型,所以可以使用钻石符号缩短这些语句:
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8); OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
要创建泛型接口,遵循与创建泛型类相同的约定。
参数化类型
您还可以用参数化类型(即List
)替换类型参数(即K
或V
)。例如,使用OrderedPair
示例:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
原始类型
原文:
docs.oracle.com/javase/tutorial/java/generics/rawTypes.html
原始类型是没有任何类型参数的泛型类或接口的名称。例如,给定泛型Box
类:
public class Box<T> { public void set(T t) { /* ... */ } // ... }
要创建Box
的参数化类型,您需要为形式类型参数T
提供实际类型参数:
Box<Integer> intBox = new Box<>();
如果省略实际类型参数,则创建Box
的原始类型:
Box rawBox = new Box();
因此,Box
是泛型类型Box
的原始类型。但是,非泛型类或接口类型不是原始类型。
在旧代码中会出现原始类型,因为在 JDK 5.0 之前,许多 API 类(如Collections
类)都不是泛型的。使用原始类型时,实际上获得的是泛型之前的行为 —— Box
会给您Object
。为了向后兼容,允许将参数化类型分配给其原始类型:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; // OK
但是,如果将原始类型赋给参数化类型,则会收到警告:
Box rawBox = new Box(); // rawBox is a raw type of Box<T> Box<Integer> intBox = rawBox; // warning: unchecked conversion
如果使用原始类型调用相应泛型类型中定义的泛型方法,也会收到警告:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; rawBox.set(8); // warning: unchecked invocation to set(T)
此警告显示原始类型绕过了泛型类型检查,将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型。
类型擦除部分提供了有关 Java 编译器如何使用原始类型的更多信息。
未经检查的错误消息
如前所述,在将旧代码与泛型代码混合使用时,可能会遇到类似以下的警告消息:
Note: Example.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
当使用在原始类型上操作的旧 API 时,可能会出现以下示例中所示的情况:
public class WarningDemo { public static void main(String[] args){ Box<Integer> bi; bi = createBox(); } static Box createBox(){ return new Box(); } }
“未经检查”一词表示编译器没有足够的类型信息来执行确保类型安全所需的所有类型检查。默认情况下,“未经检查”警告是禁用的,尽管编译器会给出提示。要查看所有“未经检查”警告,请使用-Xlint:unchecked
重新编译。
使用-Xlint:unchecked
重新编译前面的示例,会显示以下额外信息:
WarningDemo.java:4: warning: [unchecked] unchecked conversion found : Box required: Box<java.lang.Integer> bi = createBox(); ^ 1 warning
要完全禁用未经检查的警告,请使用-Xlint:-unchecked
标志。@SuppressWarnings("unchecked")
注解可以抑制未经检查的警告。如果您不熟悉@SuppressWarnings
语法,请参阅注解。
泛型方法
原文:
docs.oracle.com/javase/tutorial/java/generics/methods.html
泛型方法 是引入自己类型参数的方法。这类似于声明一个泛型类型,但类型参数的范围仅限于声明它的方法。允许静态和非静态泛型方法,以及泛型类构造方法。
泛型方法的语法包括一个类型参数列表,在方法返回类型之前出现在尖括号内。对于静态泛型方法,类型参数部分必须出现在方法返回类型之前。
Util
类包含一个泛型方法 compare
,用于比较两个 Pair
对象:
public class Util { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } } public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
调用这个方法的完整语法如下:
Pair<Integer, String> p1 = new Pair<>(1, "apple"); Pair<Integer, String> p2 = new Pair<>(2, "pear"); boolean same = Util.<Integer, String>compare(p1, p2);
类型已经明确提供,如粗体所示。通常情况下,这部分可以省略,编译器会推断所需的类型:
Pair<Integer, String> p1 = new Pair<>(1, "apple"); Pair<Integer, String> p2 = new Pair<>(2, "pear"); boolean same = Util.compare(p1, p2);
这个特性被称为类型推断,允许您将泛型方法作为普通方法调用,而无需在尖括号之间指定类型。这个主题在下一节 类型推断 中进一步讨论。
Java 中文官方教程 2022 版(六)(2)https://developer.aliyun.com/article/1486308