vlambda博客
学习文章列表

JDK17 |java17学习 第 2 章 Java 面向对象编程 OOP

Chapter 3: Java Fundamentals

本章向您展示了 Java 作为一种语言的更详细的视图。它从包中的代码组织和类(接口)的可访问性级别及其方法和属性(字段)的描述开始。引用类型,作为 Java 面向对象性质的主要类型,也被详细介绍,然后是保留和限制关键字的列表,并讨论它们的用法。本章以不同原始类型之间以及从原始类型到相应引用类型再返回的方法结束。

这些是 Java 语言的基本术语和特性。理解它们的重要性怎么强调都不为过。没有它们,您将无法编写任何 Java 程序。所以,尽量不要急于阅读本章,并确保您理解所介绍的所有内容。

本章将涵盖以下主题:

  • 包、导入和访问
  • Java 引用类型
  • 保留关键字和受限关键字
  • thissuper 关键字的使用
  • 原始类型之间的转换
  • 在原始类型和引用类型之间转换

Technical requirements

为了能够执行本章提供的代码示例,您将需要以下内容:

  • 装有 Microsoft Windows、Apple macOS 或 Linux 操作系统的计算机
  • Java SE 版本 17 或更高版本
  • 您喜欢的 IDE 或代码编辑器

第 1 章Java 17 入门,本书的。本章的代码示例文件可在 GitHub 存储库中获得,地址为 https:// /github.com/PacktPublishing/Learn-Java-17-Programming.gitexamples/src/main/java/com/packt/learnjava/ch03_fundamentals 文件夹中.

Packages, importing, and access

如您所知,包名反映了一个目录结构,从包含 .java 文件的项目目录开始。每个 .java 文件的名称必须与其中声明的顶级类的名称相同(该类可以包含其他类)。 .java 文件的第一行是以 package 关键字开头的包语句,后面是实际的包名——此文件的目录路径,其中斜杠替换为点。

包名 和类名共同构成完全限定类名。它唯一地标识类,但往往太长且使用不方便。这是 importing 来救援的时候,只允许指定一次 完全限定名称,然后只引用类按类名。

仅当调用者有权访问该类及其方法时,才能从另一个类的方法调用该类的方法。 publicprotectedprivate 访问修饰符定义可访问性级别并允许(或禁止)某些方法、属性甚至类本身对其他类可见。

所有这些方面都将在本节中详细讨论。

Packages

让我们看看我们称为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 文件在文件系统。

Importing

包声明之后,接下来是 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< /code> 属于?此外,不同包中的两个类可能具有相同的名称。如果是这种情况,组导入可能会造成一定程度的混乱,甚至是难以确定的问题。

也可以导入单独的静态类(或接口)成员。例如,如果 SomeInterface 有一个 NAME 属性(提醒一下,接口属性默认是公共的和静态的),您可以通常这样称呼它:

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
    }
}

但是应该明智地使用这种技术,因为它可能给人一种静态导入的方法或属性属于当前类的印象。

Access modifiers

我们的示例中已经使用了三个访问修饰符——publicprotectedprivate - 规范从外部对类、接口及其成员的访问 - 从其他类或接口。还有一个 第四个隐式(也称为 默认修饰符 package-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 {}
}

请注意静态嵌套类无权访问顶级类的其他成员。

内部类的另一个特殊特性是它可以访问顶级类的所有成员,甚至是私有成员,反之亦然。为了演示此功能,让我们在顶级类和 private 内部 类中创建以下私有属性和方法:

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 类可以访问顶级类的私有成员,而顶级类可以访问其内部类的私有成员。唯一的限制是非静态内部类不能有静态成员。相比之下,静态嵌套类可以同时具有静态和非静态成员,这使得静态嵌套类更加有用。

为了演示所描述的所有可能性,我们将以下 main() 方法添加到 AccessModifiers 类:

public static void main(String... args){
    var top = new AccessModifiers();
    top.topLevelPrivateMethod();
    //var inner = new InnerClass();  //compiler error
    System.out.println(InnerStaticClass.PROP);
    var inner = new InnerStaticClass();
    System.out.println(inner.innerPrivateProperty);
    inner.innerPrivateMethod();
}

自然,不能从顶级类的静态上下文访问非静态内部类,因此在前面的代码中会出现 compiler error 注释。如果我们运行它,结果将如下所示:

JDK17 |java17学习 第 2 章 Java 面向对象编程 OOP

输出的前两行来自 topLevelPrivateMethod(),其余来自 main() 方法。如您所见,内部类和顶级类可以访问彼此的私有状态,而无法从外部访问。

Java reference types

new 运算符 创建一个类的 对象并返回对对象所在的内存。从实际的角度来看,保存此引用的变量在代码中被视为对象本身。这样的变量可以是类、接口、数组或 null literal 表示没有分配内存引用到变量。如果引用的类型是接口,则可以将其分配为 null 或对实现此接口的类的对象的引用,因为接口本身无法实例化。

JVM 监视所有创建的对象并检查当前执行的代码中是否存在对它们中的每一个的引用。如果有一个对象没有对其进行任何引用,则 JVM 会在名为 garbage collection 的 进程中将其从内存中删除。我们将在第 9 章中描述这个过程/a>,JVM 结构和垃圾收集。例如,一个对象是在方法执行期间创建的,并由局部变量引用。一旦方法完成执行,这个引用就会消失。

您已经看过自定义类和接口的示例,我们已经讨论过 String 类(参见 第 1 章Java 17 入门) .在本节中,我们还将描述另外两种 Java 引用类型——数组和枚举——并演示如何使用它们。

Class and interface

一个类类型的变量是使用相应的类名声明的:

<Class name> identifier;

可以分配给此类变量的值可以是以下之一:

  • 一个 null 字面量引用类型(这意味着变量可以使用但不引用任何对象)
  • 对同一类的对象或其任何后代的引用(因为后代继承了其所有祖先的类型)

最后一种类型的赋值是,称为加宽赋值,因为它迫使专门化的引用变得不那么专门化。例如,由于每个 Java 类都是 java.lang.Object 的子类,因此可以对任何类进行以下赋值:

Object obj = new AnyClassName();

这样的赋值也被称为upcasting,因为它将变量的类型向上移动到继承行(例如任何家谱,通常在顶部显示最古老的祖先)。

在这样的向上转换之后,可以使用 (type) 转换运算符进行缩小分配:

AnyClassName anyClassName = (AnyClassName)obj;

这种分配也称为向下转换,允许您恢复后代类型。要应用此操作,您必须确保标识符实际上是指后代类型。如果有疑问,您可以使用 instanceof 运算符(参见 第 2 章Java 面向对象编程 (OOP))检查引用类型。

类似地,如果一个类实现了一个某个接口,它的对象引用可以分配给这个接口或该接口的任何祖先:

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 多态性的另一个演示。

Array

array 是一个引用类型 ,因此扩展了 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}};

唯一的要求是在使用维度之前必须对其进行初始化。

Enum

enum 参考 type 类扩展了 java.lang.Enum 类,而后者又扩展了 java.lang.Object。它允许指定一组有限的常量,每个常量都是相同类型的实例。这种集合的声明以 enum 关键字开始。这是一个例子:

enum Season { SPRING, SUMMER, AUTUMN, WINTER }

列出的每个项目 - SPRINGSUMMERAUTUMN 和 < code class="literal">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< /code>,例如)。
  • toString():默认返回与 name() 方法相同的值,但可以重写以返回任何其他 String 值。
  • ordinal():返回 enum 常量在声明时的位置(列表中的第一个有 0 序数值)。
  • valueOf(Class enumType, String name):按名称返回enum常量对象,表示为字符串文字。
  • values():静态方法,在valueOff()方法的文档中描述如下:“所有的常量enum 类可以通过调用该类的隐式 public static T[] values() 方法获得。”

为了演示前面的方法,我们将使用已经熟悉的enumSeason< /代码>:

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() 方法,让我们创建 Season1 enum:

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 + ")";
    }
}

如果我们遍历 Season2 enum 的值,结果如下:

for(Season2 s: Season2.values()){
    System.out.print(s.toString() + " "); 
          //prints: Spring(42) Summer(67) Autumn(32) Winter(20)
}

在标准 Java 库中, 有几个 enum 类 - 例如,java.time .Monthjava.time.DayOfWeekjava.util.concurrent.TimeUnit

Default values and literals

正如我们已经看到的,引用类型的默认值是null。一些消息来源称其为 特殊类型 null,但 Java 语言规范 将其限定为文字。当实例属性或引用类型的数组自动初始化时(未显式赋值时),赋值为null

除了 null 文字之外的唯一引用类型是 String 类。我们在第 1 章Java 17 入门

A reference type as a method parameter

将原始类型值传递给方法时,我们会使用它。如果我们不喜欢传递给方法的值,我们会根据需要更改它并且不要三思而后行:

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

这是一个很大的区别,不是吗?因此,您必须小心不要修改传入的对象以避免不良影响。但是,此效果偶尔会用于返回结果。但它不属于最佳实践列表,因为它降低了代码的可读性。更改传入的对象就像使用一个难以注意到的秘密隧道。因此,仅在必要时使用它。

即使传入的对象是一个包装原始值的类,这种效果仍然成立(我们将在原始类型和引用类型之间的转换中讨论原始值包装类型部分)。这是 DemoClass1modifyParameter() 方法的重载版本:

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));
}

如果我们使用 preceding 方法,结果如下:

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 值在为其分配另一个值的方法之后不会更改。这正是我们在 String 值不变性功能的目的="ch02lvl1sec02">第 1 章Java 17 入门

equals() method

等式运算符 (==),当应用于 的变量时引用类型,比较引用本身,而不是对象的内容(状态)。但是两个对象总是有不同的内存引用,即使它们有相同的内容。即使用于 String 对象,运算符 (==) 也会返回 false 如果其中至少一个是使用 new 运算符创建的(请参阅 String 值不变性的讨论"https://subscription.packtpub.com/book/programming/9781803241432/2" linkend="ch02lvl1sec02">第 1 章Java 17 入门)。

要比较内容,您可以 使用 equals() 方法。它在 String 类和数值类型包装类(IntegerFloat,等等)正是这样做的——比较对象的内容。

然而,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 类,在 上,首先比较引用,如果 < /a>references 不是同一个值,比较对象的内容:

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 可以帮助我们做到这一点,如下:

  1. 在右大括号 (}) 之前的类中单击鼠标右键。
  2. 选择生成,然后按照提示进行操作。

最终,将生成两个方法并将其添加到类中:

@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) {          返回(a == b) || (a != null && a.equals(b));      }

正如我们之前所展示的,在 String 类中实现的 equals() 方法通过内容比较字符串并提供服务目的是因为 DemoClassgetProp() 方法返回一个字符串。

hashCode() 方法 - 整数由这个 ="_idIndexMarker432">方法唯一标识这个特定对象(但请不要期望它在应用程序的不同运行之间是相同的)。如果唯一需要的方法是 equals(),则不需要实现此方法。尽管如此,还是建议使用它,以防万一这个类的对象将被收集到 Set 或基于哈希码的另一个集合中(我们将讨论第 6 章中的 Java 集合, 数据结构、泛型和流行实用程序)。

这两种方法都在 Object 中实现,因为许多算法使用 equals()hashCode( ) 方法,如果没有实现这些方法,您的应用程序可能无法运行。同时,您的对象在您的应用程序中可能不需要它们。但是,一旦 您决定实现 equals() 方法,最好实现 hasCode() 方法。此外,正如您所见,IDE 可以在没有任何开销的情况下执行此操作。

Reserved and restricted keywords

关键字是对编译器有特殊意义的词,不能用作标识符。从 Java 17 开始,有 52 个保留关键字、5 个保留标识符、3 个保留字和 10 个受限关键字。保留关键字不能在 Java 代码中的任何地方用作标识符,而受限关键字只能在模块声明的上下文中用作标识符。

Reserved keywords

以下 是所有 Java 保留关键字的列表:

JDK17 |java17学习 第 2 章 Java 面向对象编程 OOP

到目前为止,您应该对前面的大多数关键字感到宾至如归。通过练习,您可以浏览列表并检查您记住了多少。直到现在,我们还没有讨论过以下八个关键字:

  • constgoto 是保留的,但目前还没有使用。
  • assert 关键字是 assert 语句中使用的(我们将讨论这在 第 4 章中, 异常处理)。
  • synchronized 关键字 用于并发编程(我们将在 第 8 章多线程和并发处理< /em>)。
  • volatile 关键字 使变量的值不可缓存。
  • transient 关键字使 变量的值不可序列化。
  • strictfp 关键字 限制浮点计算,使其在浮点变量中执行操作时在每个平台上都得到相同的结果.
  • native 关键字 声明在平台相关代码(例如 C 或 C++)中实现的方法。

Reserved identifiers

Java中的五个保留标识符如下:

  • 许可
  • 记录
  • 密封
  • var
  • 产量

Reserved words for literal values

Java中的三个保留字如下:

  • null

Restricted keywords

Java中的10个限制关键字如下:

  • 打开
  • 模块
  • 需要
  • 传递
  • 导出
  • 打开
  • 使用
  • 提供
  • with

它们被称为restricted,因为它们不能是上下文中的标识符模块声明,我们不会在本书中讨论。在所有其他地方,可以将它们用作标识符,例如:

String to = "To";
String with = "abc";

尽管可以,但最好不要将它们用作标识符,即使在模块声明之外也是如此。

Usage of the this and super keywords

this 关键字 提供对当前对象的引用。 super 关键字指的是父类对象。这些关键字允许我们引用在当前上下文和父对象中具有相同名称的变量或方法。

Usage of the this keyword

这是 最流行的例子:

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 允许将值分配给实例属性。将 this 添加到行 2 并没有什么不同,但使用 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关键字的用法,这就是我们要讲的接下来讨论。

Usage of the super keyword

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
    }
}

随着本书的深入,我们将看到更多使用 thissuper 关键字的示例。

Converting between primitive types

最大数值类型可以容纳的数值取决于分配给它的位数。以下是每种数字类型表示的位数:

  • 字节:8位
  • char:16 位
  • short:16 位
  • int:32 位
  • long:64 位
  • 浮点数:32位
  • double:64 位

当一种数值类型的值被赋值给另一种数值类型的变量并且新类型可以 持有更大的数字时,这种转换称为扩大转化率。否则,它 是一个窄化转换,它 通常需要类型转换,使用cast 运算符。

Widening conversion

根据 到 Java 语言规范,有 19 种扩展的原始类型转换:

  • byteshort, int, longfloatdouble
  • shortint, long, float,或 double
  • charint, long, float,或 double
  • intlongfloat
  • longfloatdouble
  • floatdouble

整数类型之间以及从某些整数类型到浮点类型的扩展转换 期间,结果值与原始值匹配确切地。但是,从 int 转换为 float,从 long 转换为 float,或从 longdouble 可能会导致精度损失。根据 Java 语言规范,可以使用 IEEE 754 round-to-nearest 模式 正确舍入生成的浮点值。以下是一些证明精度损失的示例:

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 

如您所见,从 intdouble 的转换保留了值,但 long float,或 longdouble,可能会丢失精度。这取决于价值有多大。因此,请注意并允许 某些 精度损失,如果这对您的计算很重要。

Narrowing conversion

Java 语言规范 确定了 22 个缩小原语转换:

  • shortbytechar
  • charbyteshort
  • intbyteshort字符
  • longbyteshort charint
  • floatbyte, short, charintlong
  • doublebyte, short, charintlongfloat

加宽转换类似,收窄转换可能会导致精度损失,甚至会导致价值量级的损失。缩小转换比扩大转换更复杂,我们不会在本书中讨论它。重要的是要记住,在执行缩小之前,您必须确保原始值小于目标类型的最大值。否则,您可以获得完全不同的值(幅度丢失)。看下面的例子:

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 类型与 byteshort 之间的转换types 是一个更复杂的过程,因为 char 类型是无符号数字类型,而 byteshort 类型是有符号数字类型,因此即使某个值看起来适合目标类型,也可能会丢失一些信息。

Methods of conversion

除了 转换之外,每个原始类型都有一个对应的引用类型(称为 包装类),它具有 方法将此类型的值转换为任何其他基本类型,booleanchar 除外.所有的包装类都属于 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

它们中的每一个——除了 BooleanCharacter 类——都扩展了 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

示例中,请注意接受 property 参数的两个方法。这两个和其他包装类的类似方法将系统属性(如果存在)转换为相应的原始类型。

每个包装类都有 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

包装类具有许多其他有用的方法,可以将一种原始类型转换为另一种类型以及不同的格式。因此,如果您需要执行此类操作,请先查看相应的包装类。

Converting between primitive and reference types

原始类型值到相应包装类的对象的 转换称为装箱。此外,从包装类的对象到相应的原始类型值的转换称为拆箱。

Boxing

原始类型的装箱可以自动完成(称为自动装箱)或显式使用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 

请注意,自动装箱只能与相应的包装器类型相关。否则,编译器会产生错误。

ByteShortvalueOf()方法的输入值包装器需要强制转换,因为它是我们在上一节中讨论的原始类型的缩小。

Unboxing

拆箱 可以使用每个包装类中实现的 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.0
long l3 = i1;                //implicit unboxing
System.out.println(l3);      //prints: 42
double d3 = i1;              //implicit unboxing
System.out.println(d3);      //prints: 42.0

从示例中的注释中可以看到,从包装器类型到对应的原始类型的转换没有调用< strong class="bold">自动拆箱;它被称为隐式拆箱。与 自动装箱相比,即使在不匹配的包装类型和原始类型之间也可以使用隐式拆箱。

Summary

在本章中,您了解了 Java 包是什么以及它们在组织代码和类可访问性方面所起的作用,包括 import 语句和访问修饰符。您还熟悉了引用类型——类、接口、数组和枚举。任何引用类型的默认值为 null,包括 String 类型。

您现在应该了解引用类型是通过引用传递给方法的,以及 equals() 方法的使用方式和可以被覆盖的方式。您还有机会学习保留和限制关键字的完整列表,并了解 thissuper 的含义和用法关键词。

本章最后描述了原始类型、包装类型和 String 字面量之间的转换过程和方法。

在下一章中,我们将讨论 Java 异常框架、已检查和未检查(运行时)异常、try-catch-finally 块、throws throw 语句,以及异常处理的最佳实践。

Quiz

  1. 选择所有正确的陈述:
    1. Package 语句描述类或接口的位置。
    2. Package 语句描述类或接口名称。
    3. Package 是一个完全限定的名称。
    4. Package 名和类名组成了类的完全限定名。
  2. 选择所有正确的陈述:
    1. Import 语句允许使用完全限定名称。
    2. Import 语句必须是 .java 文件中的第一个。
    3. Group import 语句只引入一个包的类(和接口)。
    4. Import 语句允许避免使用完全限定名称。
  3. 选择所有正确的陈述:
    1. 如果没有访问修饰符,则该类只能由同一包的其他类和接口访问。
    2. 私有类的私有方法可以被同一个.java文件中声明的其他类访问。​​
    3. 私有类的公共方法可以被未在同一个 .java 文件中声明但来自同一个包的其他类访问。​​
    4. 受保护的方法只能由类的后代访问。
  4. 选择所有正确的陈述:
    1. 私有方法可以重载,但不能被覆盖。
    2. 受保护的方法可以被覆盖,但不能被重载。
    3. 没有访问修饰符的方法可以被覆盖和重载。
    4. 私有方法可以访问同一类的私有属性。
  5. 选择所有正确的陈述:
    1. 缩小和向下转换是同义词。
    2. 加宽和向下转换是同义词。
    3. 扩大和向上是同义词。
    4. 扩大和缩小与向上和向下没有共同之处。
  6. 选择所有正确的陈述:
    1. Array 是一个对象。
    2. Array 的长度是它可以容纳的元素的数量。
    3. 数组的第一个元素的索引为 1。
    4. 数组的第二个元素的索引为 1。
  7. 选择所有正确的陈述:
    1. Enum 包含常量。
    2. Enum 总是有一个构造函数,无论是默认的还是显式的。
    3. enum 常量可以有属性。
    4. Enum 可以有任何引用类型的常量。
  8. 选择所有正确的陈述:
    1. 可以修改作为参数传入的任何引用类型。
    2. 可以修改作为参数传入的 new String() 对象。
    3. 不能修改作为参数传入的对象引用值。
    4. 作为参数传入的数组可以将元素分配给不同的值。
  9. 选择所有正确的陈述:
    1. 不能使用保留关键字。
    2. 受限关键字不能用作标识符。
    3. 保留的 identifier 关键字不能用作标识符。
    4. 保留关键字不能用作标识符。
  10. 选择所有正确的陈述:
    1. this 关键字指的是 current 类。
    2. super 关键字指的是 super 类。
    3. thissuper 关键字指的是对象。
    4. thissuper 关键字指的是方法。
  11. 选择所有正确的陈述:
    1. 原始类型的扩展使值更大。
    2. 原始类型的缩小总是会改变值的类型。
    3. 原始类型的扩展只能在缩小转换后进行。
    4. 缩小使值更小。
  12. 选择所有正确的陈述:
    1. 装箱限制了价值。
    2. 拆箱创造新价值。
    3. 装箱创建一个引用类型的对象。
    4. 拆箱会删除引用类型的对象。