方法引用
原文:
docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
你可以使用 lambda 表达式来创建匿名方法。然而,有时候 lambda 表达式仅仅是调用一个已存在的方法。在这种情况下,通过名称引用现有方法通常更清晰。方法引用使你能够做到这一点;它们是紧凑、易读的 lambda 表达式,用于已经有名称的方法。
再次考虑在 lambda 表达式部分讨论的Person
类:
public class Person { // ... LocalDate birthday; public int getAge() { // ... } public LocalDate getBirthday() { return birthday; } public static int compareByAge(Person a, Person b) { return a.birthday.compareTo(b.birthday); } // ... }
假设你的社交网络应用的成员被包含在一个数组中,并且你想按年龄对数组进行排序。你可以使用以下代码(在示例MethodReferencesTest
中找到本节描述的代码片段):
Person[] rosterAsArray = roster.toArray(new Person[roster.size()]); class PersonAgeComparator implements Comparator<Person> { public int compare(Person a, Person b) { return a.getBirthday().compareTo(b.getBirthday()); } } Arrays.sort(rosterAsArray, new PersonAgeComparator());
此次调用sort
的方法签名如下:
static <T> void sort(T[] a, Comparator<? super T> c)
注意Comparator
接口是一个函数式接口。因此,你可以使用 lambda 表达式来代替定义并创建一个实现Comparator
的类的新实例:
Arrays.sort(rosterAsArray, (Person a, Person b) -> { return a.getBirthday().compareTo(b.getBirthday()); } );
然而,比较两个Person
实例的出生日期的方法Person.compareByAge
已经存在。你可以在 lambda 表达式的主体中调用这个方法:
Arrays.sort(rosterAsArray, (a, b) -> Person.compareByAge(a, b) );
因为这个 lambda 表达式调用了一个已存在的方法,你可以使用方法引用代替 lambda 表达式:
Arrays.sort(rosterAsArray, Person::compareByAge);
方法引用Person::compareByAge
在语义上与 lambda 表达式(a, b) -> Person.compareByAge(a, b)
相同。它们各自具有以下特征:
- 其形式参数列表是从
Comparator.compare
复制的,即(Person, Person)
。 - 其主体调用方法
Person.compareByAge
。
方法引用的种类
有四种方法引用的种类:
种类 | 语法 | 示例 |
引用静态方法 | *ContainingClass*::*staticMethodName* |
Person::compareByAge MethodReferencesExamples::appendStrings |
引用特定对象的实例方法 | *containingObject*::*instanceMethodName* |
myComparisonProvider::compareByName myApp::appendStrings2 |
引用特定类型的任意对象的实例方法 | *ContainingType*::*methodName* |
String::compareToIgnoreCase String::concat |
引用构造函数 | *ClassName*::new |
HashSet::new |
以下示例,MethodReferencesExamples
,包含了前三种方法引用的示例:
import java.util.function.BiFunction; public class MethodReferencesExamples { public static <T> T mergeThings(T a, T b, BiFunction<T, T, T> merger) { return merger.apply(a, b); } public static String appendStrings(String a, String b) { return a + b; } public String appendStrings2(String a, String b) { return a + b; } public static void main(String[] args) { MethodReferencesExamples myApp = new MethodReferencesExamples(); // Calling the method mergeThings with a lambda expression System.out.println(MethodReferencesExamples. mergeThings("Hello ", "World!", (a, b) -> a + b)); // Reference to a static method System.out.println(MethodReferencesExamples. mergeThings("Hello ", "World!", MethodReferencesExamples::appendStrings)); // Reference to an instance method of a particular object System.out.println(MethodReferencesExamples. mergeThings("Hello ", "World!", myApp::appendStrings2)); // Reference to an instance method of an arbitrary object of a // particular type System.out.println(MethodReferencesExamples. mergeThings("Hello ", "World!", String::concat)); } }
所有的System.out.println()
语句都打印相同的内容:Hello World!
BiFunction
是java.util.function
包中许多函数接口之一。BiFunction
函数接口可以表示接受两个参数并产生结果的 lambda 表达式或方法引用。
静态方法引用
方法引用Person::compareByAge
和MethodReferencesExamples::appendStrings
是对静态方法的引用。
引用特定对象的实例方法
下面是引用特定对象实例方法的示例:
class ComparisonProvider { public int compareByName(Person a, Person b) { return a.getName().compareTo(b.getName()); } public int compareByAge(Person a, Person b) { return a.getBirthday().compareTo(b.getBirthday()); } } ComparisonProvider myComparisonProvider = new ComparisonProvider(); Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
方法引用myComparisonProvider::compareByName
调用myComparisonProvider
对象的compareByName
方法。JRE 推断方法类型参数,本例中为(Person, Person)
。
类似地,方法引用myApp::appendStrings2
将调用myApp
对象的appendStrings2
方法。JRE 推断方法类型参数,本例中为(String, String)
。
引用特定类型任意对象的实例方法
下面是一个引用特定类型任意对象的实例方法的示例:
String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" }; Arrays.sort(stringArray, String::compareToIgnoreCase);
方法引用String::compareToIgnoreCase
的等效 lambda 表达式将具有形式参数列表(String a, String b)
,其中a
和b
是用于更好描述此示例的任意名称。方法引用将调用a.compareToIgnoreCase(b)
方法。
类似地,方法引用String::concat
将调用a.concat(b)
方法。
构造函数引用
你可以通过使用名称new
来引用构造函数,与引用静态方法的方式相同。以下方法将元素从一个集合复制到另一个集合:
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>> DEST transferElements( SOURCE sourceCollection, Supplier<DEST> collectionFactory) { DEST result = collectionFactory.get(); for (T t : sourceCollection) { result.add(t); } return result; }
函数接口Supplier
包含一个名为get
的方法,不接受参数并返回一个对象。因此,你可以使用 lambda 表达式调用方法transferElements
,如下所示:
Set<Person> rosterSetLambda = transferElements(roster, () -> { return new HashSet<>(); });
你可以使用构造函数引用来替代 lambda 表达式,如下所示:
Set<Person> rosterSet = transferElements(roster, HashSet::new);
Java 编译器推断你想要创建一个包含类型为Person
的元素的HashSet
集合。或者,你可以按照以下方式指定:
Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);
何时使用嵌套类、局部类、匿名类和 Lambda 表达式
原文:
docs.oracle.com/javase/tutorial/java/javaOO/whentouse.html
如在嵌套类一节中所述,嵌套类使您能够逻辑地将仅在一个地方使用的类分组,增加封装的使用,并创建更易读和可维护的代码。局部类、匿名类和 Lambda 表达式也具有这些优点;但是,它们旨在用于更具体的情况:
- 局部类:如果需要创建一个类的多个实例、访问其构造函数或引入一个新的命名类型(例如,因为您需要稍后调用其他方法),请使用它。
- 匿名类:如果需要声明字段或额外方法,请使用它。
- Lambda 表达式:
- 如果您要封装要传递给其他代码的单个行为单元,请使用它。例如,如果您希望对集合的每个元素执行某个操作,当进程完成时,或者当进程遇到错误时,您将使用 Lambda 表达式。
- 如果需要一个功能接口的简单实例,并且前述条件均不适用(例如,您不需要构造函数、命名类型、字段或额外方法),请使用它。
- 嵌套类:如果您的需求类似于局部类,并且希望使类型更广泛可用,且不需要访问局部变量或方法参数时,请使用它。
- 如果需要访问封闭实例的非公共字段和方法,请使用非静态嵌套类(或内部类)。如果不需要此访问权限,请使用静态嵌套类。
问题和练习:嵌套类
原文:
docs.oracle.com/javase/tutorial/java/javaOO/QandE/nested-questions.html
问题
- 程序
Problem.java
无法编译。你需要做什么才能使其编译?为什么? - 使用 Java API 文档中
Box
类(位于javax.swing
包中)的文档来帮助回答以下问题。
Box
定义了哪个静态嵌套类?Box
定义了哪个内部类?Box
的内部类的超类是什么?- 从任何类中可以使用
Box
的哪些嵌套类? - 如何创建
Box
的Filler
类的实例?
练习
- 获取文件
Class1.java
。编译并运行Class1
。输出是什么? - 以下练习涉及修改类
DataStructure.java
,该类在内部类示例部分讨论。
- 定义一个名为
print(DataStructureIterator iterator)
的方法。使用EvenIterator
类的实例调用此方法,使其执行与printEven
方法相同的功能。 - 调用方法
print(DataStructureIterator iterator)
,使其打印具有奇数索引值的元素。使用匿名类作为方法的参数,而不是接口DataStructureIterator
的实例。 - 定义一个名为
print(java.util.function.Function iterator)
的方法,执行与print(DataStructureIterator iterator)
相同的功能。使用 lambda 表达式调用此方法,以打印具有偶数索引值的元素。再次使用 lambda 表达式调用此方法,以打印具有奇数索引值的元素。 - 定义两个方法,使得以下两个语句打印具有偶数索引值和具有奇数索引值的元素:
DataStructure ds = new DataStructure() // ... ds.print(DataStructure::isEvenIndex); ds.print(DataStructure::isOddIndex);
检查你的答案。
枚举类型
枚举类型是一种特殊的数据类型,允许变量成为一组预定义的常量之一。变量必须等于为其预定义的值之一。常见示例包括罗盘方向(NORTH、SOUTH、EAST 和 WEST 的值)和一周的天数。
由于它们是常量,枚举类型字段的名称必须是大写字母。
在 Java 编程语言中,您可以使用enum
关键字定义枚举类型。例如,您可以指定一个星期几的枚举类型如下:
public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }
每当需要表示一组固定常量时,都应该使用枚举类型。这包括自然枚举类型,如我们太阳系中的行星和在编译时知道所有可能值的数据集,例如菜单上的选项、命令行标志等。
这里是一些代码,向您展示如何使用上面定义的Day
枚举:
public class EnumTest { Day day; public EnumTest(Day day) { this.day = day; } public void tellItLikeItIs() { switch (day) { case MONDAY: System.out.println("Mondays are bad."); break; case FRIDAY: System.out.println("Fridays are better."); break; case SATURDAY: case SUNDAY: System.out.println("Weekends are best."); break; default: System.out.println("Midweek days are so-so."); break; } } public static void main(String[] args) { EnumTest firstDay = new EnumTest(Day.MONDAY); firstDay.tellItLikeItIs(); EnumTest thirdDay = new EnumTest(Day.WEDNESDAY); thirdDay.tellItLikeItIs(); EnumTest fifthDay = new EnumTest(Day.FRIDAY); fifthDay.tellItLikeItIs(); EnumTest sixthDay = new EnumTest(Day.SATURDAY); sixthDay.tellItLikeItIs(); EnumTest seventhDay = new EnumTest(Day.SUNDAY); seventhDay.tellItLikeItIs(); } }
输出为:
Mondays are bad. Midweek days are so-so. Fridays are better. Weekends are best. Weekends are best.
Java 编程语言的枚举类型比其他语言中的对应类型更强大。enum
声明定义了一个类(称为枚举类型)。枚举类体可以包括方法和其他字段。编译器在创建枚举时会自动添加一些特殊方法。例如,它们具有一个静态values
方法,返回一个包含枚举值的数组,按照它们声明的顺序排列。此方法通常与 for-each 结构结合使用,以遍历枚举类型的值。例如,下面Planet
类示例中的代码遍历太阳系中的所有行星。
for (Planet p : Planet.values()) { System.out.printf("Your weight on %s is %f%n", p, p.surfaceWeight(mass)); }
注意: 所有枚举隐式扩展java.lang.Enum
。因为一个类只能扩展一个父类(参见声明类),Java 语言不支持状态的多重继承(参见状态、实现和类型的多重继承),因此枚举不能扩展其他任何内容。
在下面的示例中,Planet
是一个表示太阳系行星的枚举类型。它们定义了常量质量和半径属性。
每个枚举常量都声明了质量和半径参数的值。这些值在创建常量时传递给构造函数。Java 要求常量在任何字段或方法之前定义。此外,当存在字段和方法时,枚举常量列表必须以分号结尾。
注意: 枚举类型的构造函数必须是包私有或私有访问。它会自动创建在枚举体开头定义的常量。您不能自己调用枚举构造函数。
除了其属性和构造函数外,Planet
还有一些方法,可以让你获取每个行星上物体的表面重力和重量。以下是一个示例程序,它接受你在地球上的体重(以任何单位)并计算并打印出你在所有行星上的体重(以相同单位):
public enum Planet { MERCURY (3.303e+23, 2.4397e6), VENUS (4.869e+24, 6.0518e6), EARTH (5.976e+24, 6.37814e6), MARS (6.421e+23, 3.3972e6), JUPITER (1.9e+27, 7.1492e7), SATURN (5.688e+26, 6.0268e7), URANUS (8.686e+25, 2.5559e7), NEPTUNE (1.024e+26, 2.4746e7); private final double mass; // in kilograms private final double radius; // in meters Planet(double mass, double radius) { this.mass = mass; this.radius = radius; } private double mass() { return mass; } private double radius() { return radius; } // universal gravitational constant (m3 kg-1 s-2) public static final double G = 6.67300E-11; double surfaceGravity() { return G * mass / (radius * radius); } double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); } public static void main(String[] args) { if (args.length != 1) { System.err.println("Usage: java Planet <earth_weight>"); System.exit(-1); } double earthWeight = Double.parseDouble(args[0]); double mass = earthWeight/EARTH.surfaceGravity(); for (Planet p : Planet.values()) System.out.printf("Your weight on %s is %f%n", p, p.surfaceWeight(mass)); } }
如果你在命令行中运行 Planet.class
并带上参数 175,你会得到以下输出:
$ java Planet 175 Your weight on MERCURY is 66.107583 Your weight on VENUS is 158.374842 Your weight on EARTH is 175.000000 Your weight on MARS is 66.279007 Your weight on JUPITER is 442.847567 Your weight on SATURN is 186.552719 Your weight on URANUS is 158.397260 Your weight on NEPTUNE is 199.207413
问题和练习:枚举类型
原文:
docs.oracle.com/javase/tutorial/java/javaOO/QandE/enum-questions.html
问题
- 真或假:
Enum
类型可以是java.lang.String
的子类。
练习
- 重写问题和练习:类中的
Card
类,使其使用枚举类型表示卡牌的等级和花色。 - 重写
Deck
类。
检查你的答案。
课程:注解
原文:
docs.oracle.com/javase/tutorial/java/annotations/index.html
注解,一种元数据形式,提供关于程序的数据,这些数据不是程序本身的一部分。注解对其注释的代码的操作没有直接影响。
注解有多种用途,其中包括:
- 编译器的信息 — 编译器可以使用注解来检测错误或抑制警告。
- 编译时和部署时处理 — 软件工具可以处理注解信息以生成代码、XML 文件等。
- 运行时处理 — 一些注解可以在运行时被检查。
本课程解释了注解可以在哪里使用,如何应用注解,在 Java 平台标准版(Java SE API)中有哪些预定义的注解类型可用,如何将类型注解与可插入类型系统结合使用以编写具有更强类型检查的代码,以及如何实现重复注解。
注解基础知识
原文:
docs.oracle.com/javase/tutorial/java/annotations/basics.html
注解的格式
在其最简单的形式下,注解看起来像下面这样:
@Entity
符号@
告诉编译器接下来是一个注解。在下面的例子中,注解的名称是Override
:
@Override void mySuperMethod() { ... }
注解可以包括元素,这些元素可以是命名的或未命名的,并且这些元素有值:
@Author( name = "Benjamin Franklin", date = "3/27/2003" ) class MyClass { ... }
或
@SuppressWarnings(value = "unchecked") void myMethod() { ... }
如果只有一个名为value
的元素,则名称可以省略,如:
@SuppressWarnings("unchecked") void myMethod() { ... }
如果注解没有元素,则括号可以省略,如前面的@Override
示例所示。
也可以在同一声明上使用多个注解:
@Author(name = "Jane Doe") @EBook class MyClass { ... }
如果注解具有相同的类型,则称为重复注解:
@Author(name = "Jane Doe") @Author(name = "John Smith") class MyClass { ... }
从 Java SE 8 发布开始支持重复注解。更多信息,请参见重复注解。
注解类型可以是 Java SE API 的java.lang
或java.lang.annotation
包中定义的类型之一。在前面的示例中,Override
和SuppressWarnings
是预定义的 Java 注解。还可以定义自己的注解类型。前面示例中的Author
和Ebook
注解是自定义注解类型。
Java 中文官方教程 2022 版(四)(2)https://developer.aliyun.com/article/1486287