本章向读者展示了 Java 作为一种语言的更详细的视图。从包中的代码组织、类(接口)的可访问性级别及其方法和属性(字段)的描述入手,详细介绍了 Java 面向对象的主要类型&引用类型,并给出了保留关键字和限制关键字的列表,讨论了它们的用法。本章最后介绍了原始类型之间的转换方法,以及从原始类型到相应引用类型的转换方法。
这些是 Java 语言的基本术语和特性。他们理解的重要性怎么强调都不为过。没有它们,就不能编写任何 Java 程序。所以,尽量不要匆匆读完这一章,确保你理解了所有的内容。
本章将讨论以下主题:
- 包、导入和访问
- Java 引用类型
- 保留和限制关键字
this
和super
关键字的用法- 在原始类型之间转换
- 在原始类型和引用类型之间转换
如您所知,包名反映了目录结构,从包含.java
文件的项目目录开始。每个.java
文件的名称必须与其中声明的顶级类的名称相同(该类可以包含其他类)。.java
文件的第一行是package
语句,该语句以package
关键字开头,后跟实际的包名—指向此文件的目录路径,其中斜杠替换为点
包名和类名一起构成一个完全限定类名。它唯一地标识类,但往往太长,使用起来不方便。也就是说,当导入成功时,只允许指定一次完全限定名,然后只通过类名引用类。
只有调用方能够访问某个类及其方法时,才能从另一个类的方法调用该类的方法。访问修饰符public
、protected
和private
定义了可访问性级别,并允许(或不允许)某些方法、属性,甚至类本身对其他类可见。
本节将详细讨论所有这些方面。
让我们看看我们称之为Packages
的类:
package com.packt.learnjava.ch03_fundamentals;
import com.packt.learnjava.ch02_oop.hiding.C;
import com.packt.learnjava.ch02_oop.hiding.D;
public class Packages {
public void method(){
C c = new C();
D d = new D();
}
}
Packages
类中的第一行是一个包声明,它标识源树上的类位置,或者换句话说,文件系统中的.java
文件位置。在编译类并生成包含字节码的.class
文件时,包名还反映了文件系统中的.class
文件位置
在包声明之后,import
语句如下。从前面的示例中可以看出,它们允许避免在当前类的任何其他位置使用完全限定的类(或接口)名称。当导入来自同一个包的多个类(和接口)时,可以使用符号*
将来自同一个包的所有类和接口作为一个组导入。在我们的示例中,它如下所示:
import com.packt.learnjava.ch02_oop.hiding.*;
但这不是推荐的做法,因为当几个包作为一个组导入时,它会隐藏导入的类(和接口)位置。例如,请看以下代码段:
package com.packt.learnjava.ch03_fundamentals;
import com.packt.learnjava.ch02_oop.*;
import com.packt.learnjava.ch02_oop.hiding.*;
public class Packages {
public void method(){
C c = new C();
D d = new D();
}
}
在前面的代码中,您能猜出类C
或类D
属于哪个包吗?另外,不同包中的两个类可能具有相同的名称。如果是这样,组导入可能会造成混乱,甚至是难以解决的问题。
也可以导入单个静态类(或接口)成员。例如,如果SomeInterface
有一个NAME
属性(提醒您,接口属性默认为public
和static
),您通常可以如下引用它:
package com.packt.learnjava.ch03_fundamentals;
import com.packt.learnjava.ch02_oop.SomeInterface;
public class Packages {
public void method(){
System.out.println(SomeInterface.NAME);
}
}
为了避免使用接口名称,可以使用静态导入:
package com.packt.learnjava.ch03_fundamentals;
import static com.packt.learnjava.ch02_oop.SomeInterface.NAME;
public class Packages {
public void method(){
System.out.println(NAME);
}
}
类似地,如果SomeClass
具有公共静态属性someProperty
和公共静态方法someMethod()
,则也可以静态地导入它们:
package com.packt.learnjava.ch03_fundamentals;
import com.packt.learnjava.ch02_oop.StaticMembers.SomeClass;
import com.packt.learnjava.ch02_oop.hiding.C;
import com.packt.learnjava.ch02_oop.hiding.D;
import static com.packt.learnjava.ch02_oop.StaticMembers
.SomeClass.someMethod;
import static com.packt.learnjava.ch02_oop.StaticMembers
.SomeClass.SOME_PROPERTY;
public class Packages {
public static void main(String... args){
C c = new C();
D d = new D();
SomeClass obj = new SomeClass();
someMethod(42);
System.out.println(SOME_PROPERTY); //prints: abc
}
}
但是应该明智地使用这种技术,因为它可能会造成静态导入的方法或属性属于当前类的印象。
我们已经在我们的示例中使用了三个访问修饰符-public
、protected
和private
-它们控制对类、接口和,还有第四个隐式的(也称为默认修饰符包级private
),当没有指定三个显式访问修饰符时应用。
它们的使用效果非常简单:
public
:可访问当前包和其他包的其他类和接口protected
:只允许同一个包的其他成员和该类的子级访问- 无访问修饰符表示仅可由同一包的其他成员访问
private
:只允许同一类成员访问
从类或接口内部,所有的类或接口成员总是可以访问的。此外,正如我们已经多次声明的那样,除非声明为private
,否则所有接口成员在默认情况下都是公共的。
另外,请注意,类可访问性取代了类成员的可访问性,因为如果类本身不能从某个地方访问,那么对其方法或属性的可访问性的任何更改都不能使它们可访问。
当人们谈论类和接口的访问修饰符时,他们指的是在其他类或接口中声明的类和接口。包含的类或接口称为顶级类或接口,其中的类或接口称为内部类或接口。静态内部类也称为静态嵌套类。
声明顶级类或接口private
是没有意义的,因为它不能从任何地方访问。Java 作者决定不允许顶级类或接口也被声明protected
。但是,有一个没有显式访问修饰符的类是可能的,这样就使得它只能被同一个包的成员访问。
举个例子:
public class AccessModifiers {
String prop1;
private String prop2;
protected String prop3;
public String prop4;
void method1(){ }
private void method2(){ }
protected void method3(){ }
public void method4(){ }
class A1{ }
private class A2{ }
protected class A3{ }
public class A4{ }
interface I1 {}
private interface I2 {}
protected interface I3 {}
public interface I4 {}
}
请注意,静态嵌套类无权访问顶级类的其他成员。
*内部类的另一个特殊特性是它可以访问顶级类的所有成员,甚至私有成员,反之亦然。为了演示此功能,让我们在顶级类和私有内部类中创建以下私有属性和方法:
public class AccessModifiers {
private String topLevelPrivateProperty = "Top-level private value";
private void topLevelPrivateMethod(){
var inner = new InnerClass();
System.out.println(inner.innerPrivateProperty);
inner.innerPrivateMethod();
}
private class InnerClass {
//private static String PROP = "Inner static"; //error
private String innerPrivateProperty = "Inner private value";
private void innerPrivateMethod(){
System.out.println(topLevelPrivateProperty);
}
}
private static class InnerStaticClass {
private static String PROP = "Inner private static";
private String innerPrivateProperty = "Inner private value";
private void innerPrivateMethod(){
var top = new AccessModifiers();
System.out.println(top.topLevelPrivateProperty);
}
}
}
如您所见,前面类中的所有方法和属性都是私有的,这意味着通常不能从类外部访问它们。对于AccessModifiers
类也是如此:它的私有方法和属性对于在它之外声明的其他类是不可访问的。但是InnerClass
类可以访问顶级类的私有成员,而顶级类可以访问其内部类的私有成员。唯一的限制是非静态内部类不能有静态成员。相比之下,静态嵌套类可以同时具有静态和非静态成员,这使得静态嵌套类更加可用。
为了演示所描述的所有可能性,我们在类AccessModifiers
中添加了以下main()
方法:
public static void main(String... args){
var top = new AccessModifiers();
top.topLevelPrivateMethod();
//var inner = new InnerClass(); //error
System.out.println(InnerStaticClass.PROP);
var inner = new InnerStaticClass();
System.out.println(inner.innerPrivateProperty);
inner.innerPrivateMethod();
}
自然地,不能从顶级类的静态上下文访问非静态内部类,因此前面代码中的注释是无效的。如果我们运行它,结果如下:
输出的前两行来自topLevelPrivateMethod()
,其余来自main()
方法。如您所见,内部类和顶级类可以访问彼此的私有状态,从外部无法访问。
new
操作符创建一个类的对象,并返回对该对象所在内存的引用。从实际的角度来看,保存此引用的变量在代码中被视为对象本身。此类变量的类型可以是类、接口、数组或指示未向该变量分配内存引用的null
文本。如果引用的类型是一个接口,则可以将其分配给null
或对实现该接口的类的对象的引用,因为接口本身无法实例化。
JVM 监视所有创建的对象,并检查当前执行的代码中是否有对每个对象的引用。如果有一个对象没有任何引用,JVM 会在名为垃圾收集的进程中将其从内存中移除。我们将在第 9 章、“JVM 结构和垃圾收集”中描述这个过程。例如,在方法执行期间创建了一个对象,并由局部变量引用。此引用将在方法完成执行后立即消失。
您已经看到了定制类和接口的示例,我们已经讨论了String
类(参见第 1 章、“Java12 入门”)。在本节中,我们还将描述另外两种 Java 引用类型数组和枚举,并演示如何使用它们
类类型的变量使用相应的类名声明:
<Class name> identifier;
可分配给此类变量的值可以是以下值之一:
- 引用类型字面值
null
(表示可以使用变量,但不引用任何对象) - 对同一类的对象或其任何子对象的引用(因为子对象继承其所有祖先的类型)
最后一种类型的赋值被称为加宽赋值,因为它迫使一个特化的引用变得不那么专业化。例如,由于每个 Java 类都是java.lang.Object
的子类,因此可以对任何类进行以下赋值:
Object obj = new AnyClassName();
这种赋值也被称为向上转型,因为它将变量的类型在继承线上上移(与任何家谱树一样,通常在最上面显示最早的祖先)。
在这样的向上转型之后,可以使用转型操作符(type)
进行缩小分配:
AnyClassName anyClassName = (AnyClassName)obj;
这样的赋值也称为向下转型,允许您恢复子体类型。要应用此操作,必须确保标识符实际上引用了子体类型。如果有疑问,可以使用instanceof
操作符(参见第 2 章、"Java 面向对象编程")检查引用类型。
类似地,如果类实现某个接口,则可以将其对象引用指定给该接口或该接口的任何祖先:
interface C {}
interface B extends C {}
class A implements B { }
B b = new A();
C c = new A();
A a1 = (A)b;
A a2 = (A)c;
如您所见,在类引用向上转换和向下转换的情况下,在将对象的引用分配给某个实现接口类型的变量之后,可以恢复该对象的原始类型
本节的内容也可以看作 Java 多态的另一个实际演示。
数组是一种引用类型,因此也扩展了java.lang.Object
类。数组元素的类型与声明的数组类型相同。元素的数目可以是零,在这种情况下,数组被称为空数组。每个元素都可以被一个索引访问,索引是正整数或零。第一个元素的索引为零。元素的数量称为数组长度。数组一旦创建,其长度就不会改变。
以下是数组声明的示例:
int[] intArray;
float[][] floatArray;
String[] stringArray;
SomeClass[][][] arr;
每个括号对表示另一个维度。括号对的数目是数组的嵌套深度:
int[] intArray = new int[10];
float[][] floatArray = new float[3][4];
String[] stringArray = new String[2];
SomeClass[][][] arr = new SomeClass[3][5][2];
new
操作符为以后可以赋值(填充)的每个元素分配内存。但是数组的元素在创建时被初始化为默认值,如下例所示:
System.out.println(intArray[3]); //prints: 0
System.out.println(floatArray[2][2]); //prints: 0.0
System.out.println(stringArray[1]); //prints: null
创建数组的另一种方法是使用数组初始化器,即用逗号分隔的值列表,每个维度都用大括号括起来。例如:
int[] intArray = {1,2,3,4,5,6,7,8,9,10};
float[][] floatArray ={{1.1f,2.2f,3,2},{10,20.f,30.f,5},{1,2,3,4}};
String[] stringArray = {"abc", "a23"};
System.out.println(intArray[3]); //prints: 4
System.out.println(floatArray[2][2]); //prints: 3.0
System.out.println(stringArray[1]); //prints: a23
可以创建多维数组,而无需声明每个维度的长度。只有第一个维度必须指定长度:
float[][] floatArray = new float[3][];
System.out.println(floatArray.length); //prints: 3
System.out.println(floatArray[0]); //prints: null
System.out.println(floatArray[1]); //prints: null
System.out.println(floatArray[2]); //prints: null
//System.out.println(floatArray[3]); //error
//System.out.println(floatArray[2][2]); //error
其他尺寸的缺失长度可以稍后指定:
float[][] floatArray = new float[3][];
floatArray[0] = new float[4];
floatArray[1] = new float[3];
floatArray[2] = new float[7];
System.out.println(floatArray[2][5]); //prints: 0.0
这样,就可以为不同的尺寸指定不同的长度。使用数组初始化器,还可以创建不同长度的维度:
float[][] floatArray ={{1.1f},{10,5},{1,2,3,4}};
唯一的要求是在使用维度之前必须对其进行初始化。
枚举引用类型类扩展了java.lang.Enum
类,后者又扩展了java.lang.Object
。它允许指定一组有限的常量,每个常量都是同一类型的实例。此类集合的声明以关键字enum
开始。举个例子:
enum Season { SPRING, SUMMER, AUTUMN, WINTER }
所列的每一项–SPRING
、SUMMER
、AUTUMN
和WINTER
–都是Season
类型的实例。它们是Season
类仅有的四个实例。它们是预先创建的,可以作为Season
类型的值在任何地方使用。无法创建Season
类的其他实例。这就是创建enum
类型的原因:当一个类的实例列表必须限制为固定的集合时,可以使用它。
enum
声明也可以用驼色字母写:
enum Season { Spring, Summer, Autumn, Winter }
但是,使用全部大写样式的频率更高,因为正如我们前面提到的,有一个约定,在大写情况下表示静态最终常量的标识符。它有助于区分常量和变量。enum
常量是静态的,隐式地是最终的。
因为enum
值是常量,所以它们在 JVM 中是唯一存在的,可以通过引用进行比较:
Season season = Season.WINTER;
boolean b = season == Season.WINTER;
System.out.println(b); //prints: true
以下是java.lang.Enum
类中最常用的方法:
name()
:按声明时的拼写返回enum
常量的标识符(例如WINTER
)。toString()
:默认返回与name()
方法相同的值,但可以覆盖以返回任何其他String
值。ordinal()
:返回声明时enum
常量的位置(列表中第一个有0
序数值)。valueOf(Class enumType, String name)
:返回enum
常量对象,其名称表示为String
文本。values()
:在java.lang.Enum
类的文档中没有描述的静态方法。在《Java 语言规范 8.9.3》中,描述为隐式声明。《Java™ 教程》表示编译器在创建enum
时会自动添加一些特殊方法,其中静态values()
方法按声明顺序返回包含enum
所有值的数组。
为了演示上述方法,我们将使用已经熟悉的enum
、Season
:
enum Season { SPRING, SUMMER, AUTUMN, WINTER }
下面是演示代码:
System.out.println(Season.SPRING.name()); //prints: SPRING
System.out.println(Season.WINTER.toString()); //prints: WINTER
System.out.println(Season.SUMMER.ordinal()); //prints: 1
Season season = Enum.valueOf(Season.class, "AUTUMN");
System.out.println(season == Season.AUTUMN); //prints: true
for(Season s: Season.values()){
System.out.print(s.name() + " ");
//prints: SPRING SUMMER AUTUMN WINTER
}
为了覆盖toString()
方法,我们创建enum Season1
:
enum Season1 {
SPRING, SUMMER, AUTUMN, WINTER;
public String toString() {
return this.name().charAt(0) +
this.name().substring(1).toLowerCase();
}
}
其工作原理如下:
for(Season1 s: Season1.values()){
System.out.print(s.toString() + " ");
//prints: Spring Summer Autumn Winter
}
可以向每个enum
常量添加任何其他属性。例如,让我们为每个enum
实例添加一个平均温度值:
enum Season2 {
SPRING(42), SUMMER(67), AUTUMN(32), WINTER(20);
private int temperature;
Season2(int temperature){
this.temperature = temperature;
}
public int getTemperature(){
return this.temperature;
}
public String toString() {
return this.name().charAt(0) +
this.name().substring(1).toLowerCase() +
"(" + this.temperature + ")";
}
}
如果我们迭代enum Season2
的值,结果如下:
for(Season2 s: Season2.values()){
System.out.print(s.toString() + " ");
//prints: Spring(42) Summer(67) Autumn(32) Winter(20)
}
在标准 Java 库中,有几个enum
类。例如,java.time.Month
、java.time.DayOfWeek
、java.util.concurrent.TimeUnit
我们已经看到,引用类型的默认值是null
。一些源代码将其称为特殊类型null
,但 Java 语言规范将其限定为文本。当引用类型的实例属性或数组自动初始化时(未显式赋值时),赋值为null
除了null
字面值之外,唯一的引用类型是String
类,我们在第 1 章、“Java12 入门”中讨论了字符串。
当一个原始类型值被传递到一个方法中时,我们使用它。如果我们不喜欢传递到方法中的值,我们会根据需要进行更改,并且不会三思而后行:
void modifyParameter(int x){
x = 2;
}
我们不担心方法之外的变量值会发生变化:
int x = 1;
modifyParameter(x);
System.out.println(x); //prints: 1
无法在方法之外更改原始类型的参数值,因为原始类型参数是通过值传递到方法的。这意味着值的副本被传递到方法中,因此即使方法中的代码为其指定了不同的值,原始值也不会受到影响。
引用类型的另一个问题是,即使引用本身是通过值传递的,它仍然指向内存中相同的原始对象,因此方法中的代码可以访问该对象并修改它。为了演示它,让我们创建一个DemoClass
和使用它的方法:
class DemoClass{
private String prop;
public DemoClass(String prop) { this.prop = prop; }
public String getProp() { return prop; }
public void setProp(String prop) { this.prop = prop; }
}
void modifyParameter(DemoClass obj){
obj.setProp("Changed inside the method");
}
如果我们使用上述方法,结果如下:
DemoClass obj = new DemoClass("Is not changed");
modifyParameter(obj);
System.out.println(obj.getProp()); //prints: Changed inside the method
这是一个很大的区别,不是吗?因此,您必须小心不要修改传入的对象以避免产生不希望的效果。但是,此效果偶尔用于返回结果。但它不属于最佳实践列表,因为它会降低代码的可读性。更改传入对象就像使用一个难以注意的秘密隧道。所以,只有在必要的时候才使用它。
即使传入的对象是一个包装原始类型值的类,这种效果仍然有效(我们将在“原始和引用类型”之间的转换部分讨论原始类型值包装类型),下面是一个DemoClass1
和一个重载版本的modifyParameter()
方法:
class DemoClass1{
private Integer prop;
public DemoClass1(Integer prop) { this.prop = prop; }
public Integer getProp() { return prop; }
public void setProp(Integer prop) { this.prop = prop; }
}
void modifyParameter(DemoClass1 obj){
obj.setProp(Integer.valueOf(2));
}
如果我们使用上述方法,结果如下:
DemoClass1 obj = new DemoClass1(Integer.valueOf(1));
modifyParameter(obj);
System.out.println(obj.getProp()); //prints: 2
引用类型的这种行为的唯一例外是String
类的对象。下面是另一个重载版本的modifyParameter()
方法:
void modifyParameter(String obj){
obj = "Changed inside the method";
}
如果我们使用上述方法,结果如下:
String obj = "Is not changed";
modifyParameter(obj);
System.out.println(obj); //prints: Is not changed
obj = new String("Is not changed");
modifyParameter(obj);
System.out.println(obj); //prints: Is not changed
如您所见,无论我们使用一个字面值还是一个新的String
对象,结果都是一样的:在给它赋值的方法之后,原始的String
值没有改变。这正是我们在第 1 章“Java12 入门”中讨论的String
值不变性特性的目的
等式运算符(==
应用于引用类型的变量时,比较的是引用本身,而不是对象的内容(状态)。但是两个对象总是有不同的内存引用,即使它们有相同的内容。即使用于String
对象,如果至少有一个对象是使用new
操作符创建的,操作符(==
也会返回false
(参见第 1 章“Java12 入门”中关于String
值不变性的讨论)。
要比较内容,可以使用equals()
方法。它在String
类和数值类型包装类(Integer
、Float
等)中的实现正好可以比较对象的内容
然而,java.lang.Object
类中的equals()
方法实现只比较引用,这是可以理解的,因为子类可能拥有的内容种类繁多,而泛型内容比较的实现是不可行的。这意味着每一个需要有equals()
方法来比较对象内容而不仅仅是引用的 Java 对象都必须重新实现equals()
方法,从而在java.lang.Object
类中覆盖其实现,如下所示:
public boolean equals(Object obj) {
return (this == obj);
}
相比之下,看看同样的方法是如何在Integer
类中实现的:
private final int value;
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
如您所见,它从输入对象中提取原始int
值,并将其与当前对象的原始值进行比较。它根本不比较对象引用
另一方面,String
类首先比较引用,如果引用的值不相同,则比较对象的内容:
private final byte[] value;
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
StringLatin1.equals()
和StringUTF16.equals()
方法逐个字符比较值,而不仅仅是引用值。
类似地,如果应用代码需要按内容比较两个对象,则必须覆盖相应类中的equals()
方法。例如,让我们看看熟悉的DemoClass
类:
class DemoClass{
private String prop;
public DemoClass(String prop) { this.prop = prop; }
public String getProp() { return prop; }
public void setProp(String prop) { this.prop = prop; }
}
我们可以手动添加equals()
方法,但是 IDE 可以帮助我们完成以下操作:
- 在类中右键单击右大括号(
}
) - 选择“生成”,然后按照提示进行操作
最终,将生成两个方法并将其添加到类中:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DemoClass)) return false;
DemoClass demoClass = (DemoClass) o;
return Objects.equals(getProp(), demoClass.getProp());
}
@Override
public int hashCode() {
return Objects.hash(getProp());
}
通过查看生成的代码,我们希望您注意以下几点:
@Override
注解的用法:它确保该方法覆盖某个祖先中的方法(具有相同的签名)。有了这个注解,如果您修改了方法并更改了签名(错误地或有意地),编译器(和您的 IDE)将立即引发一个错误,告诉您在任何祖先类中都没有具有这种签名的方法。因此,它有助于及早发现错误。java.util.Objects
类的用法:它有很多非常有用的方法,包括equals()
静态方法,它不仅比较引用,还使用equals()
方法:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
因为,正如我们前面所演示的,在String
类中实现的equals()
方法根据字符串的内容进行比较,符合我们的目的,因为DemoClass
的方法getProp()
返回一个字符串
hashCode()
方法:这个方法返回的整数唯一地标识这个特定的对象(但是请不要期望它在应用的不同运行之间是相同的)。如果唯一需要的方法是equals()
,则不需要实现此方法。尽管如此,我们还是建议在Set
或基于哈希码的另一个集合中收集此类的对象时使用它(我们将在第 6 章、“数据结构、泛型和流行工具”中讨论 Java 集合)
这两种方法都在Object
中实现,因为许多算法使用equals()
和hashCode()
方法,如果没有实现这些方法,应用可能无法工作。同时,对象在应用中可能不需要它们。但是,一旦您决定实现equals()
方法,也可以实现hasCode()
方法。此外,正如您所看到的,IDE 可以做到这一点而不需要任何开销。
关键字是对编译器有特殊意义的词,不能用作标识符。保留关键字 51 个,限制关键字 10 个。保留关键字不能在 Java 代码中的任何地方用作标识符,而受限关键字只能在模块声明的上下文中用作标识符。
以下是所有 Java 保留关键字的列表:
abstract |
assert |
boolean |
break |
byte |
case |
catch |
char |
class |
const |
continue |
default |
do |
double |
else |
enum |
extends |
final |
finally |
float |
for |
if |
goto |
implements |
import |
instanceof |
int |
interface |
long |
native |
new |
package |
private |
protected |
public |
return |
short |
static |
strictfp |
super |
switch |
synchronized |
this |
throw |
throws |
transient |
try |
void |
volatile |
while |
下划线(_
也是一个保留字。
到现在为止,您应该对前面的大多数关键字都很熟悉了。通过一个练习,你可以浏览一下清单,看看你记得其中有多少。我们不仅仅讨论了以下八个关键词:
const
和goto
已保留,但尚未使用assert
关键字用于assert
语句中(我们将在第 4 章、“处理”中讨论)synchronized
关键字用于并发编程(我们将在第 8 章、“多线程和并发处理”中讨论)volatile
关键字使变量的值不被缓存transient
关键字使变量的值不可序列化strictfp
关键字限制浮点计算,使得在对浮点变量执行操作时,每个平台上的结果相同- 关键字 AutoT0:Audio 声明了一种在依赖于平台的代码中实现的方法,如 C 或 C++。
Java 中的 10 个受限关键字如下:
open
module
requires
transitive
exports
opens
to
uses
provides
with
它们被称为受限,因为它们不能作为模块声明上下文中的标识符,这在本书中我们将不讨论。在所有其他地方,都可以将它们用作标识符。例如:
String to = "To";
String with = "abc";
尽管可以,但最好不要将它们用作标识符,即使是在模块声明之外
this
关键字提供对当前对象的引用。super
关键字引用父类对象。这些关键字允许我们引用在当前上下文和父对象中具有相同名称的变量或方法。
下面是最流行的例子:
class A {
private int count;
public void setCount(int count) {
count = count; // 1
}
public int getCount(){
return count; // 2
}
}
第一行看起来模棱两可,但事实上并非如此:局部变量int count
隐藏实例私有属性int count
。我们可以通过运行以下代码来演示:
A a = new A();
a.setCount(2);
System.out.println(a.getCount()); //prints: 0
使用this
关键字修复问题:
class A {
private int count;
public void setCount(int count) {
this.count = count; // 1
}
public int getCount(){
return this.count; // 2
}
}
将this
添加到第 1 行允许将值赋给实例属性。在第 2 行中添加this
并没有什么区别,但是每次都使用this
关键字和instance
属性是一个很好的做法。它使代码更具可读性,并有助于避免难以跟踪的错误,例如我们刚刚演示的错误。
我们也看到了equals()
方法中的this
关键字用法:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DemoClass)) return false;
DemoClass demoClass = (DemoClass) o;
return Objects.equals(getProp(), demoClass.getProp());
}
并且,为了提醒您,下面是我们在第 2 章、“Java 面向对象编程(OOP)”中介绍的构造器示例:
class TheChildClass extends TheParentClass{
private int x;
private String prop;
private String anotherProp = "abc";
public TheChildClass(String prop){
super(42);
this.prop = prop;
}
public TheChildClass(int arg1, String arg2){
super(arg1);
this.prop = arg2;
}
// methods follow
}
在前面的代码中,您不仅可以看到this
关键字,还可以看到super
关键字的用法,我们将在下面讨论。
super
关键字引用父对象。我们已经在“构造器中的this
关键字的用法”部分中看到了它的用法,因为必须先创建父类对象,然后才能创建当前对象。如果构造器的第一行不是super()
,则表示父类有一个没有参数的构造器。
当方法被覆盖并且必须调用父类的方法时,super
关键字特别有用:
class B {
public void someMethod() {
System.out.println("Method of B class");
}
}
class C extends B {
public void someMethod() {
System.out.println("Method of C class");
}
public void anotherMethod() {
this.someMethod(); //prints: Method of C class
super.someMethod(); //prints: Method of B class
}
}
随着本书的深入,我们将看到更多使用this
和super
关键字的例子。
一个数值类型可以容纳的最大数值取决于分配给它的位数。以下是每种数字表示形式的位数:
byte
:8 位char
:16 位short
:16 位int
:32 位long
:64 位float
:32 位double
:64 位
当一个数值类型的值被分配给另一个数值类型的变量,并且新类型可以容纳更大的数值时,这种转换被称为加宽转换。否则,它是一个缩小转换,通常需要使用cast
操作符进行类型转换
根据 Java 语言规范,有 19 种基本类型转换:
byte
至short
、int
、long
、float
或double
short
至int
、long
、float
或double
char
至int
、long
、float
或double
int
至long
、float
或double
long
至float
或double
float
至double
在整数类型之间以及从某些整数类型到浮点类型的加宽转换过程中,生成的值与原始值完全匹配。然而,从int
到float
,或从long
到float
,或从long
到double
的转换可能会导致精度损失。根据 Java 语言规范,产生的浮点值可以使用IEEE 754 round-to-nearest mode
正确舍入。以下几个例子说明了精度的损失:
int i = 123456789;
double d = (double)i;
System.out.println(i - (int)d); //prints: 0
long l1 = 12345678L;
float f1 = (float)l1;
System.out.println(l1 - (long)f1); //prints: 0
long l2 = 123456789L;
float f2 = (float)l2;
System.out.println(l2 - (long)f2); //prints: -3
long l3 = 1234567891111111L;
double d3 = (double)l3;
System.out.println(l3 - (long)d3); //prints: 0
long l4 = 12345678999999999L;
double d4 = (double)l4;
System.out.println(l4 - (long)d4); //prints: -1
如您所见,从int
到double
的转换保留了值,但是long
到float
或long
到double
可能会失去精度。这取决于这个值有多大。所以,如果它对你的计算很重要的话,请注意并考虑到精度的损失。
Java 语言规范确定了 22 种缩小原始类型转换:
short
至byte
或char
char
至byte
或short
int
至byte
、short
或char
long
至byte
、short
、char
或int
float
至byte
、short
、char
、int
或long
double
至byte
、short
、char
、int
、long
或float
与加宽转换类似,变窄转换可能导致精度损失,甚至值幅度损失。缩小的转换比扩大的转换更复杂,在本书中我们将不讨论它。请务必记住,在执行缩小之前,必须确保原始值小于目标类型的最大值。否则,您可以得到完全不同的值(丢失幅值)。请看以下示例:
System.out.println(Integer.MAX_VALUE); //prints: 2147483647
double d1 = 1234567890.0;
System.out.println((int)d1); //prints: 1234567890
double d2 = 12345678909999999999999.0;
System.out.println((int)d2); //prints: 2147483647
从示例中可以看出,不必首先检查目标类型是否可以容纳该值,就可以得到正好等于目标类型的最大值的结果。剩下的就要丢了,不管差别有多大。
在执行缩小转换之前,请检查目标类型的最大值是否可以保持原始值。
请注意,char
类型和byte
或short
类型之间的转换是一个更复杂的过程,因为char
类型是无符号数字类型,而byte
和short
类型是有符号数字类型,所以即使值看起来像它符合目标类型。
除了转换之外,每个原始类型都有一个对应的引用类型(称为包装类),该类具有将该类型的值转换为除boolean
和char
之外的任何其他原始类型的方法。所有包装类都属于java.lang
包:
java.lang.Boolean
java.lang.Byte
java.lang.Character
java.lang.Short
java.lang.Integer
java.lang.Long
java.lang.Float
java.lang.Double
除了类Boolean
和Character
之外,它们都扩展了抽象类java.lang.Number
,抽象类有以下抽象方法:
byteValue()
shortValue()
intValue()
longValue()
floatValue()
doubleValue()
这样的设计迫使Number
类的后代实现所有这些。它们产生的结果与前面示例中的cast
运算符相同:
int i = 123456789;
double d = Integer.valueOf(i).doubleValue();
System.out.println(i - (int)d); //prints: 0
long l1 = 12345678L;
float f1 = Long.valueOf(l1).floatValue();
System.out.println(l1 - (long)f1); //prints: 0
long l2 = 123456789L;
float f2 = Long.valueOf(l2).floatValue();
System.out.println(l2 - (long)f2); //prints: -3
long l3 = 1234567891111111L;
double d3 = Long.valueOf(l3).doubleValue();
System.out.println(l3 - (long)d3); //prints: 0
long l4 = 12345678999999999L;
double d4 = Long.valueOf(l4).doubleValue();
System.out.println(l4 - (long)d4); //prints: -1
double d1 = 1234567890.0;
System.out.println(Double.valueOf(d1)
.intValue()); //prints: 1234567890
double d2 = 12345678909999999999999.0;
System.out.println(Double.valueOf(d2)
.intValue()); //prints: 2147483647
此外,每个包装器类都有允许将数值的String
表示转换为相应的原始数值类型或引用类型的方法。例如:
byte b1 = Byte.parseByte("42");
System.out.println(b1); //prints: 42
Byte b2 = Byte.decode("42");
System.out.println(b2); //prints: 42
boolean b3 = Boolean.getBoolean("property");
System.out.println(b3); //prints: false
Boolean b4 = Boolean.valueOf("false");
System.out.println(b4); //prints: false
int i1 = Integer.parseInt("42");
System.out.println(i1); //prints: 42
Integer i2 = Integer.getInteger("property");
System.out.println(i2); //prints: null
double d1 = Double.parseDouble("3.14");
System.out.println(d1); //prints: 3.14
Double d2 = Double.valueOf("3.14");
System.out.println(d2); //prints: 3.14
在示例中,请注意接受参数属性的两种方法。这两种方法以及其他包装类的类似方法将系统属性(如果存在)转换为相应的原始类型。
并且每个包装器类都有一个toString(primitive value)
静态方法来将原始类型值转换为它的String
表示。例如:
String s1 = Integer.toString(42);
System.out.println(s1); //prints: 42
String s2 = Double.toString(3.14);
System.out.println(s2); //prints: 3.14
包装器类还有许多其他有用的方法,可以将一种原始类型转换为另一种原始类型和不同的格式。因此,如果您需要这样做,请首先查看相应的包装器类。
将原始类型值转换为相应包装类的对象称为装箱。此外,从包装类的对象到相应的原始类型值的转换被称为拆箱。
原始类型的装箱可以自动补全(称为自动装箱),也可以显式使用每个包装器类型中可用的valueOf()
方法完成:
int i1 = 42;
Integer i2 = i1; //autoboxing
//Long l2 = i1; //error
System.out.println(i2); //prints: 42
i2 = Integer.valueOf(i1);
System.out.println(i2); //prints: 42
Byte b = Byte.valueOf((byte)i1);
System.out.println(b); //prints: 42
Short s = Short.valueOf((short)i1);
System.out.println(s); //prints: 42
Long l = Long.valueOf(i1);
System.out.println(l); //prints: 42
Float f = Float.valueOf(i1);
System.out.println(f); //prints: 42.0
Double d = Double.valueOf(i1);
System.out.println(d); //prints: 42.0
请注意,只有在将原始类型转换为相应的包装器类型时,才能进行自动装箱。否则,编译器将生成一个错误。
Byte
和Short
包装器的方法valueOf()
的输入值需要强制转换,因为这是我们在上一节讨论过的原始类型的缩小。
拆箱可以使用在每个包装类中实现的Number
类的方法来完成:
Integer i1 = Integer.valueOf(42);
int i2 = i1.intValue();
System.out.println(i2); //prints: 42
byte b = i1.byteValue();
System.out.println(b); //prints: 42
short s = i1.shortValue();
System.out.println(s); //prints: 42
long l = i1.longValue();
System.out.println(l); //prints: 42
float f = i1.floatValue();
System.out.println(f); //prints: 42.0
double d = i1.doubleValue();
System.out.println(d); //prints: 42.0
Long l1 = Long.valueOf(42L);
long l2 = l1; //implicit unboxing
System.out.println(l2); //prints: 42
double d2 = l1; //implicit unboxing
System.out.println(d2); //prints: 42
long l3 = i1; //implicit unboxing
System.out.println(l3); //prints: 42
double d3 = i1; //implicit unboxing
System.out.println(d3); //prints: 42
从示例中的注释可以看出,从包装器类型到相应的原始类型的转换不是称为自动拆箱,而是称为隐式拆箱。与自动装箱不同的是,即使在包装和不匹配的原始类型之间也可以使用隐式拆箱。
在本章中,您了解了什么是 Java 包,以及它们在组织代码和类可访问性(包括import
语句和访问修饰符)方面所起的作用。您还熟悉了引用类型:类、接口、数组和枚举。任何引用类型的默认值为null
,包括String
类型。
现在您了解了引用类型是通过引用传递到方法中的,以及如何使用和覆盖equals()
方法。您还学习了保留关键字和限制关键字的完整列表,了解了this
和super
关键字的含义和用法。
本章最后描述了原始类型、包装类型和String
字面值之间转换的过程和方法。
在下一章中,我们将讨论 Java 异常框架、受检和非受检(运行时)异常、try-catch-finally
块、throws
和throw
语句,以及异常处理的最佳实践。
-
选择所有正确的语句:
Package
语句描述类或接口位置Package
语句描述类或接口名称Package
是一个完全限定的名称Package
名称和类名构成了类的完全限定名
-
选择所有正确的语句:
Import
语句允许使用完全限定名Import
语句必须是.java
文件中的第一个语句Group import
语句只引入一个包的类(和接口)Import statement
允许避免使用完全限定名
-
选择所有正确的语句:
- 如果没有访问修饰符,该类只能由同一包的其他类和接口访问
- 私有类的私有方法可以被同一
.java
文件中声明的其他类访问 - 私有类的
public
方法可以被不在同一.java
文件中声明但来自同一包的其他类访问 - 受保护的方法只能由类的后代访问
-
选择所有正确的语句:
- 私有方法可以重载,但不能覆盖
- 受保护的方法可以覆盖,但不能重载
- 没有访问修饰符的方法可以被覆盖和重载
- 私有方法可以访问同一类的私有属性
-
选择所有正确的语句:
- 缩小和向上转型是同义词
- 加宽和向下转型是同义词
- 加宽和向上转型是同义词
- 加宽和缩小与向上转型和向下转型没有任何共同之处
-
选择所有正确的语句:
Array
是一个对象Array
的长度是它能容纳的元素的数量- 数组的第一个元素具有索引 1
- 数组的第二个元素具有索引 1
-
选择所有正确的语句:
Enum
包含常量。Enum
总是有一个构造器,默认或显式enum
常量可以有属性Enum
可以有任何引用类型的常量
-
选择所有正确的语句:
- 可以修改作为参数传入的任何引用类型
- 作为参数传入的
new String()
对象可以修改 - 不能修改作为参数传入的对象引用值
- 作为参数传入的数组可以将元素指定给不同的值
-
选择所有正确的语句:
- 不能使用保留关键字
- 受限关键字不能用作标识符
- 保留关键字
identifier
不能用作标识符 - 保留关键字不能用作标识符
-
选择所有正确的语句: 1.
this
关键字是指current
类 2.super
关键字是指super
类 3. 关键词this
和super
指的是对象 4.this
和super
是指方法 -
选择所有正确的语句: 1. 原始类型的加宽使值变大 2. 原始类型的缩小总是会更改值的类型 3. 原始类型的加宽只能在缩小转换后进行 4. 缩小会使值变小
-
选择所有正确的语句: 1. 装箱限制了值 2. 拆箱将创建一个新值 3. 装箱创建引用类型对象 4. 拆箱将删除引用类型对象