1. Basic Java Syntax & Useful Built-in Methods
由于作者在开始本节课的学习之前已经对 Java 基本语法有一些基本的了解,该部分不会全面而详尽的叙述相关的知识,只是会简要列出一些容易混淆或忘记的知识点。
详尽的叙述可以参考 CS61B 电子课本 或 Java 教程。
类的成员分为两种,静态成员(Static Member) 与 实例成员(Instance Member),后者只能被类的实例访问;而静态成员既可以通过类名访问也可以通过实例访问,它们会访问到 同一个 对象上。然而,从代码风格的角度讲,在绝大多数情况下静态成员 都应该通过类名访问,通过实例访问静态成员是一个不好的写法。
Java 中,除了八种基本的数据类型之外,其余所有数据类型都是 引用类型(Reference Type)。每一个被声明为引用类型的变量本质上都只是一个存储着 指向该对象实例在内存中实际存储地址的指针,大小为机器字长。这些指针初始值均为 0x0,即 null。
Java 中函数只有按值传参这一种传参方式。然而,当我们传入一些指向引用类型对象实例的变量时,也能隐式的做到按引用传参。
嵌套类
Java 中的类可以嵌套。嵌套的类可以通过 static 被声明为静态类。嵌套的静态类只能访问 外围类(Enclosing Class) 中的静态属性。然而,顶级类不能被声明为静态类。
高阶函数
在 Java 7 及之前,由于不允许变量存储指向某个函数的引用,在 Java 中实现高阶函数的写法相当丑陋,需要借助接口间接实现。然而,Java 8 提供了函数式接口与 Lambda 表达式特性,使得书写高阶函数简单了很多。
函数式接口是指有且仅有一个抽象方法的接口。Java 8 提供了 java.util.function 包,其中内置了函数式接口,例如 Function<T, R>、Consumer<T>、Predicate<T> 等。这些接口与实现了这些接口的类可以直接作为参数或返回值使用。为自己书写的接口添加 @FunctionalInterface 标注可以将该接口声明为自定义的函数式接口。
常用的内置函数式接口的用法如下:
Function<T, R>接受一个类型为T的对象并返回一个类型为R的对象。Predicate<T>接受一个类型为T的对象并返回一个布尔值。Consumer<T>接受一个类型为T的对象,但没有返回值。Supplier<T>不接受任何对象,返回一个类型为T的对象。
由于函数式接口被强制规定了只能有一个抽象方法,Java 中的 Lambda 表达式与函数式接口之间可以进行隐式的转换,即定义的 Lambda 表达式将自动被视为接口所声明方法的实现。Lambda 表达式的格式如下:
(<parameters>) -> {<suite>};
当参数只有一个时,括号可以省略。参数的类型无需显示声明,此时编译器依据其被赋值给的函数式接口自行推断参数类型。
与 Python 不同而与 C++ 类似,Java 中的 Lambda 表达式的函数体可以包含多行。
Lambda 函数是 懒执行 的,具体解释可以见11. 迭代器与生成器 Iterators & Generators。
接受函数作为参数:
import java.util.function.Function;
public class HigherOrderExample {
// 高阶函数:接受一个 Function 作为参数
public static Integer applyOperation(Function<Integer, Integer> func, Integer value) {
return func.apply(value);
}
public static void main(String[] args) {
// 使用 Lambda 定义函数
Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> increment = x -> x + 1;
// 调用高阶函数
// 输出 25
System.out.println(applyOperation(square, 5));
// 输出 6
System.out.println(applyOperation(increment, 5));
}
}
返回一个函数:
import java.util.function.Function;
public class HigherOrderExample {
// 返回一个函数
public static Function<Integer, Integer> multiplyBy(Integer factor) {
return x -> x * factor;
}
public static void main(String[] args) {
Function<Integer, Integer> triple = multiplyBy(3);
System.out.println(triple.apply(5)); // 输出 15
}
}
自定义函数式接口:
@FunctionalInterface
interface StringProcessor {
String process(String input);
}
public class HigherOrderExample {
// 高阶函数:接受 StringProcessor 作为参数
public static String processString(StringProcessor processor, String input) {
return processor.process(input);
}
public static void main(String[] args) {
// 使用 Lambda 或方法引用
String result = processString(s -> s.toUpperCase(), "hello");
System.out.println(result); // 输出 "HELLO"
}
}
Java 允许通过 类::方法 的语法传递方法引用,这种写法广泛用于 Java 8 引入的另一特性 stream 中,如在 map、filter、reduce 等函数(它们的功能与 Python 中的同名函数类似)中。
import java.util.List;
import java.util.Arrays;
public class StreamExample {
public static void main(String[] args) {
List< Integer > numbers = Arrays.asList(1, 2, 3, 4, 5);
// 高阶函数应用:map 接受一个 Function
numbers.stream()
.map(x -> x * 2) // Lambda 表达式
.forEachprintln; // 输出 2,4,6,8,10
}
}
Comparable 与 Comparator
Java 有两个内置的泛型接口 Comparable<T> 与 Comparator<T>,允许为自定义类创建自定义的比较方法。二者均为函数式接口,各自定义了 public int compareTo(T obj) 与 public int compare(T o1, T o2) 方法。
Java 出于如下目的区分出这两种比较的方法:
Comparable一般用于定义对象默认的、天然的比较规则(如String按字典序,Integer按数值大小),需要修改类的源代码。类的某个实例调用compareTo方法来将 自身 与传入实例比较,依据结果:- 正数:自身更“大”
- 0:二者“相等”
- 负数:自身更“小”
假设我们需要对一个包含某个类对象的数列排序,Java 会依照实现的 compareTo 方法返回的结果将对象由“小”到“大”进行排序。所谓的“小”、“大”与“相等”只由比较方法的返回的结果确定。
因此,我们如何实现 compareTo 方法决定了我们如何看待什么样的类实例是更“大”的。一般而言,实现 compareTo 方法时,需要尽量贴近现实生活中的常识,而将各种自定义比较方法通过 Comparator 实现。
Comparator一般用于定义临时的、可扩展的比较规则(如按年龄、姓名等不同维度排序)。其无需修改类,可灵活定义多个比较器。依据结果:- 正数:
o1更“大” - 0:二者“相等”
- 负数:
o2更“大”
- 正数:
一般而言,当当对象的排序逻辑是唯一的、自然的(如数值大小、字典序),且可以修改类代码时,使用 Comparable;否则当我们需要多种排序方式(如按年龄、姓名、薪资等不同维度);无法修改类的源码(如第三方库的类);或需要动态组合排序规则(如先按年龄,再按姓名)时,使用 Comparator。
Comparator 相关的高级技巧链式排序
// 先按年龄升序,年龄相同再按姓名降序
Comparator< Person > complexComparator = Comparator
.comparingIntgetAge
.thenComparinggetName).reversed();
逆序排序
// 自然排序的逆序
Collections.sort(list, Comparator.reverseOrder());
// Comparator 的逆序
Comparator< Person > reversedAgeComparator = ageComparator.reversed();
空值处理
// 将 null 视为小于非空值
Comparator< Person > nullsFirstComparator = Comparator
.nullsFirstgetName);
Array & Set
Java 内置了 List 与 Set 接口。二者均继承自集合接口 Collections,分别规定了列表与集合两种数据结构。两个接口都规定了大量方法,具体可见 Java List、Java Set,此处不再赘述。Java 内置了实现了 List 接口的 LinkedList 类与 ArrayList 类与实现了 Set 接口的 HashSet 类与 TreeSet 类。
Iterator
与 Python 类似,Java 中同样存在迭代器,其遍历某个可迭代对象中的所有元素一次且仅一次。实现了 Iterable<T> 接口的类可以通过调用 iterator() 方法得到某个可迭代实例的迭代器,并可以使用 for (object : iterable) 循环遍历内部所有元素。
除了实现 iterator() 方法外,为了使返回的迭代器可以正常工作,还需要实现 Iterator<T> 接口的 hasNext() 与 next() 方法。
Iterable 是 Collection 的父类,而后者是 List 与 Set 等容器接口的父类。
在调用某个迭代器的 hasNext() 方法并返回 False 后继续调用 next() 方法属于 未定义行为。然而,绝大多数 Iterator::hasNext 方法的实现都会在这种情况抛出 NoSuchElementException 异常,在编写自定义迭代器时也应遵循这一点。
Object
Java 中的所有类都继承自 Object 类,无论是否显式指定继承。Object 类中的所有方法详见 Java Object。这里仅就几个常用的方法做说明:
toString()提供某个对象的字符串表示,将某个对象传递给System.out.println()输出时本质上是调用了obj.toString()并将得到的结果输出至标准输出流。默认该方法会输出该对象在内存的地址。equals()用于确定一个对象与自身是否“相等”。前面提到过,所有存储着引用类型的变量本质上储存的是指向其在内存中地址的指针。因此使用 “==” 比较两个引用类型变量一般不会得到预期结果(因为实际上比较的是存储地址是否相同)。而equals()方法就允许我们规定两个引用类型“相等”的判定方法。默认该方法与 “==” 等同。重写equals()方法需要注意的点较多,如:equals方法应当满足等价关系,即具有自反性、对称性、传递性。- 参数类型必须为
Object。 - 语句
x.equals(y)的值在x与y不发生改变前保持不变。 - 语句
x.equals(null)的值永远为False。