问题和练习:泛型
原文:
docs.oracle.com/javase/tutorial/java/generics/QandE/generics-questions.html
- 编写一个通用方法来计算集合中具有特定属性的元素数量(例如,奇数、质数、回文数)。
- 以下类会编译吗?如果不会,为什么?
public final class Algorithm { public static <T> T max(T x, T y) { return x > y ? x : y; } }
- 编写一个通用方法来交换数组中两个不同元素的位置。
- 如果编译器在编译时擦除所有类型参数,为什么应该使用泛型?
- 在类型擦除后,以下类被转换为什么?
public class Pair<K, V> { public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } private K key; private V value; }
- 以下方法在类型擦除后会转换为什么?
public static <T extends Comparable<T>> int findFirstGreaterThan(T[] at, T elem) { // ... }
- 以下方法会编译吗?如果不会,为什么?
public static void print(List<? extends Number> list) { for (Number n : list) System.out.print(n + " "); System.out.println(); }
- 编写一个通用方法来查找列表范围
begin, end)
中的最大元素。 - 以下类会编译吗?如果不会,为什么?
public class Singleton<T> { public static T getInstance() { if (instance == null) instance = new Singleton<T>(); return instance; } private static T instance = null; }
- 给定以下类:
class Shape { /* ... */ } class Circle extends Shape { /* ... */ } class Rectangle extends Shape { /* ... */ } class Node<T> { /* ... */ }
- 以下代码会编译吗?如果不会,为什么?
Node<Circle> nc = new Node<>(); Node<Shape> ns = nc;
- 考虑这个类:
class Node<T> implements Comparable<T> { public int compareTo(T obj) { /* ... */ } // ... }
- 以下代码会编译吗?如果不会,为什么?
Node<String> node = new Node<>(); Comparable<String> comp = node;
- 如何调用以下方法来找到列表中与指定整数列表互质的第一个整数?
public static <T> int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)
- 注意,两个整数a和b互质,如果 gcd(a, b) = 1,其中 gcd 是最大公约数的缩写。
[检查你的答案。
课程:包(Packages)
这节课讲解了如何将类和接口打包成包,如何使用在包中的类,以及如何安排文件系统以便编译器能够找到你的源文件。
创建和使用包
原文:
docs.oracle.com/javase/tutorial/java/package/packages.html
为了使类型更容易找到和使用,避免命名冲突,并控制访问权限,程序员将相关类型的组合打包成包。
定义: 包 是提供访问保护和命名空间管理的相关类型的分组。请注意,类型 指的是类、接口、枚举和注解类型。枚举和注解类型是类和接口的特殊种类,因此在本课程中,类型 经常简称为类和接口。
Java 平台中的类型是通过功能将类打包在一起的各种包的成员:基本类在java.lang
中,用于读写(输入和输出)的类在java.io
中,依此类推。你也可以将你的类型放在包中。
假设你编写了一组表示图形对象的类,比如圆、矩形、线条和点。你还编写了一个接口,Draggable
,如果类可以被鼠标拖动,则实现该接口。
//*in the Draggable.java file* public interface Draggable { ... } //*in the Graphic.java file* public abstract class Graphic { ... } //*in the Circle.java file* public class Circle extends Graphic implements Draggable { . . . } //*in the Rectangle.java file* public class Rectangle extends Graphic implements Draggable { . . . } //*in the Point.java file* public class Point extends Graphic implements Draggable { . . . } //*in the Line.java file* public class Line extends Graphic implements Draggable { . . . }
你应该将这些类和接口打包到一个包中,原因包括以下几点:
- 你和其他程序员可以轻松确定这些类型是相关的。
- 你和其他程序员知道在哪里找到可以提供与图形相关功能的类型。
- 你的类型名称不会与其他包中的类型名称冲突,因为包会创建一个新的命名空间。
- 你可以允许包内的类型彼此之间具有无限制的访问,但仍然限制包外类型的访问。
创建一个包
原文:
docs.oracle.com/javase/tutorial/java/package/createpkgs.html
要创建一个包,你需要为包选择一个名称(命名约定将在下一节讨论),并在包含你想要放入包中的类型(类、接口、枚举和注解类型)的每个源文件的顶部放置一个带有该名称的package
语句。
包语句(例如,package graphics;
)必须是源文件中的第一行。每个源文件中只能有一个包语句,并且它适用于文件中的所有类型。
**注意:**如果你在单个源文件中放入多个类型,只能有一个是public
的,并且它必须与源文件同名。例如,你可以在文件Circle.java
中定义public class Circle
,在文件Draggable.java
中定义public interface Draggable
,在文件Day.java
中定义public enum Day
,等等。
你可以在同一个文件中包含非公共类型和一个公共类型(这是强烈不推荐的,除非非公共类型很小并且与公共类型密切相关),但只有公共类型可以从包外访问。所有顶级的非公共类型将是包私有的。
如果你将前面部分列出的图形接口和类放在一个名为graphics
的包中,你将需要六个源文件,如下所示:
//*in the Draggable.java file* package graphics; public interface Draggable { . . . } //*in the Graphic.java file* package graphics; public abstract class Graphic { . . . } //*in the Circle.java file* package graphics; public class Circle extends Graphic implements Draggable { . . . } //*in the Rectangle.java file* package graphics; public class Rectangle extends Graphic implements Draggable { . . . } //*in the Point.java file* package graphics; public class Point extends Graphic implements Draggable { . . . } //*in the Line.java file* package graphics; public class Line extends Graphic implements Draggable { . . . }
如果你不使用package
语句,你的类型将会进入一个无名包。一般来说,无名包只适用于小型或临时应用程序,或者当你刚开始开发过程时。否则,类和接口应该放在命名包中。
包命名
原文:
docs.oracle.com/javase/tutorial/java/package/namingpkgs.html
全球范围内的程序员使用 Java 编程语言编写类和接口,很可能许多程序员会为不同类型使用相同的名称。事实上,前面的例子就是这样做的:它定义了一个 Rectangle
类,而 java.awt
包中已经有一个 Rectangle
类。尽管如此,如果它们位于不同的包中,编译器仍允许两个类具有相同的名称。每个 Rectangle
类的完全限定名称包括包名。也就是说,graphics
包中的 Rectangle
类的完全限定名称是 graphics.Rectangle
,而 java.awt
包中的 Rectangle
类的完全限定名称是 java.awt.Rectangle
。
这种方法很有效,除非两个独立的程序员使用相同的包名。如何避免这个问题?约定。
命名约定
包名全部小写以避免与类或接口的名称冲突。
公司使用其反转的互联网域名作为其包名的起始部分—例如,com.example.mypackage
表示由 example.com
的程序员创建的名为 mypackage
的包。
公司内部发生的名称冲突需要在公司内部通过约定处理,也许可以在公司名称后面加上地区或项目名称(例如,com.example.region.mypackage
)。
Java 语言中的包以 java.
或 javax.
开头。
在某些情况下,互联网域名可能不是有效的包名。如果域名包含连字符或其他特殊字符,如果包名以数字或其他 Java 名称不允许用作 Java 名称开头的字符开头,或者包名包含保留的 Java 关键字,例如 “int”。在这种情况下,建议的约定是添加下划线。例如:
合法化包名
域名 | 包名前缀 |
hyphenated-name.example.org |
org.example.hyphenated_name |
example.int |
int_.example |
123name.example.com |
com.example._123name |
使用包成员
原文:
docs.oracle.com/javase/tutorial/java/package/usepkgs.html
组成包的类型被称为包成员。
要从其包外部使用public
包成员,你必须执行以下操作之一:
- 通过其完全限定名称引用成员
- 导入包成员
- 导入成员的整个包
每种情况都适用于不同的情况,如下面的部分所解释的。
通过其限定名称引用包成员
到目前为止,在本教程中的大多数示例都通过其简单名称引用类型,如Rectangle
和StackOfInts
。如果你编写的代码与该成员在同一个包中,或者已经导入了该成员,你可以使用包成员的简单名称。
但是,如果你尝试使用来自不同包的成员,并且该包尚未被导入,你必须使用成员的完全限定名称,其中包括包名称。以下是在前面示例中声明的graphics
包中的Rectangle
类的完全限定名称。
graphics.Rectangle
你可以使用这个限定名称来创建graphics.Rectangle
的实例:
graphics.Rectangle myRect = new graphics.Rectangle();
对于不经常使用的限定名称是可以的。然而,当一个名称被重复使用时,反复输入名称变得乏味,代码变得难以阅读。作为替代方案,你可以导入成员或其包,然后使用其简单名称。
导入包成员
要将特定成员导入当前文件,请在文件开头放置一个import
语句,在任何类型定义之前,但在package
语句之后(如果有的话)。以下是如何从前一节中创建的graphics
包中导入Rectangle
类。
import graphics.Rectangle;
现在你可以通过其简单名称引用Rectangle
类。
Rectangle myRectangle = new Rectangle();
如果你只从graphics
包中使用少量成员,这种方法很有效。但如果你从一个包中使用许多类型,你应该导入整个包。
导入整个包
要导入特定包中包含的所有类型,请使用带有星号(*)
通配符的import
语句。
import graphics.*;
现在你可以通过其简单名称引用graphics
包中的任何类或接口。
Circle myCircle = new Circle(); Rectangle myRectangle = new Rectangle();
import
语句中的星号只能用于指定包中的所有类,如下所示。它不能用于匹配包中的一部分类。例如,以下内容不匹配以A
开头的graphics
包中的所有类。
// *does not work* import graphics.A*;
相反,它会生成编译器错误。通常情况下,使用import
语句只导入单个包成员或整个包。
注意: 另一种不太常见的 import
形式允许你导入封闭类的公共嵌套类。例如,如果 graphics.Rectangle
类包含有用的嵌套类,比如 Rectangle.DoubleWide
和 Rectangle.Square
,你可以通过以下两个语句导入 Rectangle
及其嵌套类。
import graphics.Rectangle; import graphics.Rectangle.*;
请注意,第二个导入语句不会导入 Rectangle
。
另一种不太常见的 import
形式,即静态导入语句,将在本节末尾讨论。
为了方便起见,Java 编译器会自动为每个源文件导入两个完整的包:(1)java.lang
包和(2)当前包(当前文件的包)。
包的表面层次结构
起初,包看起来是分层的,但实际上并非如此。例如,Java API 包括一个 java.awt
包,一个 java.awt.color
包,一个 java.awt.font
包,以及许多以 java.awt
开头的其他包。然而,java.awt.color
包、java.awt.font
包和其他 java.awt.xxxx
包不包含在 java.awt
包中。前缀 java.awt
(Java 抽象窗口工具包)用于一些相关的包,以明确显示它们之间的关系,而不是表示包含关系。
导入 java.awt.*
导入了 java.awt
包中的所有类型,但不会导入 java.awt.color
、java.awt.font
或任何其他 java.awt.xxxx
包。如果你计划使用 java.awt.color
中的类以及 java.awt
中的类,你必须导入这两个包及其所有文件:
import java.awt.*; import java.awt.color.*;
名称歧义
如果一个包中的成员与另一个包中的成员同名,并且两个包都被导入,你必须通过其限定名称引用每个成员。例如,graphics
包定义了一个名为 Rectangle
的类。java.awt
包也包含一个 Rectangle
类。如果 graphics
和 java.awt
都被导入,以下内容是模棱两可的。
Rectangle rect;
在这种情况下,你必须使用成员的完全限定名称来指示你想要的确切 Rectangle
类。例如,
graphics.Rectangle rect;
静态导入语句
有些情况下,你需要频繁访问一个或两个类的静态 final 字段(常量)和静态方法。反复添加这些类的名称可能会导致代码混乱。静态导入语句为你提供了一种导入你想要使用的常量和静态方法的方式,这样你就不需要为它们的类名添加前缀。
java.lang.Math
类定义了 PI
常量和许多静态方法,包括用于计算正弦、余弦、正切、平方根、最大值、最小值、指数等的方法。例如,
public static final double PI = 3.141592653589793; public static double cos(double a) { ... }
通常,要从另一个类中使用这些对象,你需要添加类名前缀,如下所示。
double r = Math.cos(Math.PI * theta);
使用静态导入语句可以导入 java.lang.Math 的静态成员,这样就不需要在类名Math
前加前缀了。Math
的静态成员可以单独导入:
import static java.lang.Math.PI;
或者作为一个组:
import static java.lang.Math.*;
一旦它们被导入,静态成员可以无需限定地使用。例如,前面的代码片段将变为:
double r = cos(PI * theta);
显然,你可以编写自己的类,其中包含你经常使用的常量和静态方法,然后使用静态导入语句。例如,
import static mypackage.MyConstants.*;
注意: 静态导入要非常谨慎使用。过度使用静态导入会导致代码难以阅读和维护,因为代码读者无法知道哪个类定义了特定的静态对象。正确使用静态导入可以通过消除类名重复使代码更易读。
管理源文件和类文件
原文:
docs.oracle.com/javase/tutorial/java/package/managingfiles.html
Java 平台的许多实现依赖于分层文件系统来管理源文件和类文件,尽管Java 语言规范并不要求这样做。策略如下。
将一个类、接口、枚举或注解类型的源代码放在一个文本文件中,文件名为类型的简单名称,扩展名为.java
。例如:
//in the Rectangle.java file package graphics; public class Rectangle { ... }
然后,将源文件放在一个反映类型所属包名的目录中:
.....\graphics\Rectangle.java
包成员的限定名称和文件的路径名称是平行的,假设使用 Microsoft Windows 文件名分隔符反斜杠(对于 UNIX,请使用正斜杠)。
- 类名 –
graphics.Rectangle
- 文件路径 –
graphics\Rectangle.java
正如你应该记得的那样,按照惯例,公司使用其反转的互联网域名作为其包名。例如,其互联网域名为example.com
的 Example 公司将在其所有包名之前加上com.example
。包名的每个组件对应一个子目录。因此,如果 Example 公司有一个包含Rectangle.java
源文件的com.example.graphics
包,它将包含在一系列子目录中,如下所示:
....\com\example\graphics\Rectangle.java
当你编译一个源文件时,编译器为其中定义的每个类型创建一个不同的输出文件。输出文件的基本名称是类型的名称,其扩展名是.class
。例如,如果源文件如下所示
//in the Rectangle.java file package com.example.graphics; public class Rectangle { . . . } class Helper{ . . . }
然后编译后的文件将位于:
<path to the parent directory of the output files>\com\example\graphics\Rectangle.class <path to the parent directory of the output files>\com\example\graphics\Helper.class
与.java
源文件一样,编译后的.class
文件应该在反映包名的一系列目录中。然而,.class
文件的路径不一定要与.java
源文件的路径相同。你可以将源文件和类文件目录分开管理,如:
<path_one>\sources\com\example\graphics\Rectangle.java <path_two>\classes\com\example\graphics\Rectangle.class
Java 中文官方教程 2022 版(七)(2)https://developer.aliyun.com/article/1486321