第 4 章 对象与类(下)

简介: 第 4 章 对象与类

第 4 章 对象与类(上):https://developer.aliyun.com/article/1391470

4.4 静态字段与静态方法

4.4.1 静态字段

如果将一个字段定义为 static,每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。

class Employee
{
    private static int nextId = 1;
    private int id;
}

即使没有 Employee 对象,静态字段 nextId 也存在。它属于类,而不属于任何单个的对象。


静态字段被称为类字段


4.4.2 静态常量

静态变量使用得比较少,但静态常量却很常用。例如,在 Math 类中定义一个静态常量。


可以用 Math.PI 来访问这个变量


省略关键字 static,PI 就变成了 Math 类但一个实例字段。需要通过 Math 类的一个对象来访问 PI,并且每一个 Math 对象都有它自己的一个 PI 副本


4.4.3 静态方法

静态方法是不在对象上执行的方法。例如,Math 类的 pow 方法就是一个静态方法。


Math.pow(x, a);


会计算幂 x ^ a。在万层运算时,它并不使用 Math 对象。换句话说,它没有隐式参数。


可以认为静态方法是没有 this 参数的方法


Employee 类的静态方法不能访问 id 实例字段,因为它不能在对象上执行操作。但是,静态方法可以访问静态字段。

public static int getNextId()
{
    return nextId;
}

方法可以省略关键字 static 吗?答案是肯定的。这样一来,需要通过 Employee 类对象的引用来调用这个方法。


可以使用对象调用静态方法,这是合法的。


两种情况下可以使用静态方法:


方法不需要访问对象状态,因为它需要的所有参数都通过显式参数提供

方法只需要访问类的静态字段

4.4.4 工厂方法

静态方法还有另外一种常见的用途。类似 LocalDate 和 NumberFormat 的类使用静态工厂方法(factory method)来构造对象。

NumberFormat currencyInstance = NumberFormat.getCurrencyInstance();
NumberFormat percentInstance = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyInstance.format(x));
System.out.println(percentInstance.format(x));

为什么 NumberFormat 类不利用构造器完成这些操作呢?这主要有两个原因。


无法命名构造器。构造器的名字必须与类名相同。但是,这里希望有两个不同的名字,分别得到货币实例和百分比实例。

使用构造器时,无法改变所构造对象的类型。而工厂方法实际上将返回 DecimalFormat 类的对象,这是 NumberFormat 的一个子类。

4.4.5 main 方法

main 方法也是一个静态方法


main 方法不对任何对象进行操作。事实上,在启动程序时还没有任何对象。静态的 main 方法将执行并构造程序所需对象。


4.5 方法参数

按值调用(call by value)表示方法接收的是调用者提供的值。按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。


以前还有按名调用(call by name),Algol 程序设计语言是最古老的高级程序设计语言之一,它使用的就是这种参数传递方式


Java 程序设计语言总是采用按值调用。方法得到的是所有参数值的一个副本。


有两种类型的方法参数:


基本数据类型(数字、布尔值)

对象引用

总结以下在 Java 中对方法参数能做什么和不能做什么


方法不能修改基本数据类型的参数(即数值型或布尔型)

方法可以改变对象参数的状态

方法不能让一个对象参数引用一个新的对象

方法可以通过对象引用的副本修改所引用对象的状态


4.6 对象构造

4.6.1 重载

重载(overloading),如果多个方法有相同的名字、不同的参数,便出现了重载。编译器必须挑选出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好(这个查找匹配的过程被称为重载解析(overloading resolution))


Java 允许重载任何方法,而不只是构造器方法。要完整地描述一个方法,需要指定方法名以及参数类型。这叫作方法的签名(signature)


返回类型不是方法签名的一部分。不能有两个名字相同、参数类型也相同却有不同返回类型的方法。


4.6.2 默认字段初始化

如果在构造器中没有显式地为字段设置初值,那么就会被自动地赋为默认值:数值为 0、布尔值为 false、对象引用为 null。


方法中的局部变量必须明确地初始化。在类中,如果没有初始化类中的字段,将会自动初始化为默认值。


4.6.3 无参数的构造器

很多类都包含一个无参数的构造器,由无参构造器创建对象时,对象的状态会设置为适当的默认值


如果写一个类时没有编写构造器,就会为你提供一个无参构造器。这个构造器将所有的实例字段设置为默认值。


如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的


仅当类没有任何其他构造器的时候,你才会得到一个默认的无参数构造器


4.6.4 显式字段初始化

可以在类定义中直接为任何字段赋值

class Employee
{
    private String name = "";
}

在执行构造器之前先完成这个赋值操作


利用方法调用初始化一个字段

class Employee
{
    private static int nextId;
    private in id = assignId;
    private static int assignId()
    {
        int r = nextId;
        nextId ++;
        return r;
    }
}

4.6.5 参数名

用单个字母作为参数名


在每个参数前面加上一个前缀“a”


参数变量会遮蔽同名的实例字段


4.6.6 调用另一个构造器

关键字 this 指示一个方法的隐式参数。不过,这个关键字还有另外一个含义。


如果构造器的第一个语句形如 this(...),这个构造器将调用同一个类的另一个构造器。

public Employee(double s)
{
    this("Employee #" + nextId, s);
    nextId ++;
}

当调用 new Employee(60000) 时,Employee(double) 构造器将调用 Employee(String, double) 构造器


采用这种方式使用 this 关键字非常有用,这样对公共的构造器代码只需要编写一次即可


4.6.7 初始化快

前面已经讲过两种初始化数据字段的方法:


在构造器中设置值

在声明中赋值

Java 还有第三种机制,称为初始化快(initialization block)。在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。


首先运行初始化快,然后才运行构造器的主体部分。


这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。


建议总是将初始化块放在字段定义之后。


调用构造器的具体处理步骤:


如果构造器的第一行调用另一个构造器,则基于所提供的参数执行第二个构造器

否则

所有数据字段初始化为其默认值

按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块

执行构造器主体代码

可以通过提供一个初始值,或者使用一个静态的初始化块来初始化静态字段


如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块


将代码放在一个块中,并标记关键字 static


在类第一次加载的时候,将会进行静态字段的初始化


所有静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行

package chapter3.ConstructorTest;
import java.util.Random;
public class ConstructorTest
{
    public static void main(String[] args) {
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("Harry", 40000);
        staff[1] = new Employee(60000);
        staff[2] = new Employee();
        for (Employee employee : staff) {
            System.out.println("name = " + employee.getName() + ", id = " + employee.getId() + ", salary = " + employee.getSalary());
        }
    }
}
class Employee
{
    private static int nextId;
    private int id;
    private String name = "";
    private double salary;
    static
    {
        Random generator = new Random();
        nextId = generator.nextInt(10000);
    }
    {
        id = nextId;
        nextId ++;
    }
    public Employee(String n, double s)
    {
        name = n;
        salary = s;
    }
    public Employee(double s)
    {
        this("Employee #" + nextId, s);
    }
    public Employee()
    {
    }
    public String getName()
    {
        return name;
    }
    public double getSalary()
    {
        return salary;
    }
    public int getId()
    {
        return id;
    }
}

4.6.8 对象析构与 finalize 方法

由于 Java 会完成自动的垃圾回收,不需要人工回收内存,所以 Java 不支持析构器


如果一个资源一旦使用完就需要立即关闭,那么应当提供一个 close 方法来完成必要的清理工作。可以在对象使用完时调用这个 close 方法


如果可以等到虚拟机退出,那么可以用方法 Runtime.addShutdownHook 增加一个“关闭钩”(shutdown hook)。在 Java 9 中,可以使用 Cleaner 类注册一个动作,当对象不再可达时(除了清洁器还能访问,其他对象都无法访问这个对象),就会完成这个动作。在实际中这些情况很少见。


不要使用 finalize 方法来完成清理。这个方法原本要在垃圾回收器清理对象之前调用。不过,你并不能知道这个方法到底什么时候调用,而且该方法已经被废弃。


4.7 包

使用包(package)将类组织在一个集合中


4.7.1 包名

使用包的主要原因是确保类名的唯一性


为了确保包名的绝对唯一性,要用一个因特网域名以逆序的形式作为包名,然后对于不同的工程使用不同的子包


类的“完全限定”名


从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util 包与 java.util.jar 包毫无关系。每一个包都是独立的类集合


4.7.2 类的导入

采用两种方式访问另一个包中的公共类。第一种方式就是使用完全限定名(fully qualified name);就是包名后面跟着类名。


简单且更常用的方式是使用 import 语句。import 语句是一种引用包中各个类的简捷方式。一旦使用了 import 语句,在使用类时,就不必写出类的全名了。


可以使用 import 语句导入一个特定的类或者整个包。import 语句应该位于源文件的顶部。


import java.time.*;


还可以导入一个包中的特定类:


import java.time.LocalDate;


在包中定位类是编译器(compiler)的工作。类文件中的字节码总是使用完整的包名引用其他类。


4.7.3 静态导入

import 语句允许导入静态方法和静态字段,而不只是类


源文件顶部,添加一条指令:


import static java.lang.System.*;


就可以使用 System 类的静态方法和静态字段,而不必加类名前缀:


out.println("Goodbye, World!");


exit(0);


还可以导入特定的方法或字段:


import static java.lang.System.out;


4.7.4 在包中增加类

将类放入包中,就必须将包的名字放在源文件的开头


没有在源文件中放置 package 语句,这个源文件中的类就属于无包名(unnamed package)


将源文件放到与完整包名匹配的子目录中。编译器将类文件也放在相同的目录结构中。


PackageTest 类属于无名包;Employee 类属于 com.horstmann.corejava 包。


想要编译这个程序,只需切换到基目录,并运行命令


javac PackageTest.java


编译器就会自动地查找文件 com/horstmann/corejava/Employee.java 并进行编译


4.7.5 包访问

没有指定 public 或 private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问


对于变量来说就有些不适宜类,变量必须显式地标记为 private,不然的话将默认为包可访问。显然,这样做会破坏封装性。


在默认情况下,包不是封闭的实体。也就是说,任何人都可以向包中添加更多的类。


从 1.2 版开始,JDK 的实现者修改了加载器,明确地禁止加载包名以"java."开头的用户自定义类。


4.7.6 类路径

类文件也可以存储在 JAR 文件中。在一个 JAR 文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省空间又可以改善性能。


JAR 文件使用 ZIP 格式组织文件和子目录。可以使用任何 ZIP 工具查看 JAR 文件


类路径所列出的目录和归档文件是搜索类的起始点


一个源文件只能包含一个公共类,并且文件名与公共类名必须匹配


4.7.7 设置类路径

最好使用 -classpath(或 -cp,或者 Java 9 中的 --class-path)选项指定类路径:


java -classpath /home/user/classdir


或者


java -classpath c:\classdir


整个指令必须写在一行中


利用 -classpath 选项设置类路径是首选的方法,也可以通过设置 CLASSPATH 环境变量来指定。具体细节依赖于所使用的 shell


4.8 JAR 文件

在将应用程序打包时,你一定希望只向用户提供一个单独的文件,而不是一个包含大量类文件的目录结构,Java 归档(JAR)文件就是为此目的设计的。一个 JAR 文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。JAR 文件是压缩的,它使用了我们熟悉的 ZIP 压缩格式。


4.8.1 创建 JAR 文件

可以使用 jar 工具制作 JAR 文件。创建一个新 JAR 文件最常用的命令使用以下语法:


jar cvf jarFileName file1 file2 ...


例如:


jar cvf CalculatorClasses.jar *.class icon.gif


通常,jar 命令的格式如下:


jar option file1 file2 ...


4.8.2 清单文件

除了类文件、图像和其他资源外,每个 JAR 文件还包含一个清单文件(manifest),用于描述归档文件的特殊特性


清单文件被命名为 MANIFEST.MF,它位于 JAR 文件的一个特殊的 META-INF 子目录中。符合标准的最小清单文件及其内容:


Manifest-Version: 1.0


清单条目被分成多个节。第一节被称为主节(main section)。它作用于整个 JAR 文件。随后的条目用来指定命名实体的属性,如单个文件、包或者 URL。它们都必须以一个 Name 条目开始。节与节之间用空行分开。


4.8.3 可执行 JAR 文件

可以使用 jar 命令中的 e 选项指定程序的入口点,既通常需要在调用 java 程序启动器时指定的类:


jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass files to add


可以在清单文件中指定程序的主类,包括以下形式的语句:


Main-Class: com.company.mypkg.MainAppClass


不要为主类名增加扩展名.class


清单文件的最后一行必须以换行符结束。否则,清单文件将无法被正常地读取。


用户可以简单地通过下面的命令来启动程序:


java -jar MyProgram.jar


4.8.4 多版本 JAR 文件

Java 9 引入了多版本的 JAR(multi-release JAR),其中可以包含面向不同 Java 版本的类文件


额外的类文件放在 META-INF/versions 目录中


4.8.5 关于命令行选项说明

Java 开发包(JDK)的命令行选项一直以来都使用单个短横线加多字母选项名的形式


从 Java 9 开始,Java 工具开始转向一种更常用的选项格式,多字母选项名前面加两个短横线,另外对于常用的选项可以使用单字母快捷方式


4.9 文档注释

javadoc 它可以由源文件生成一个 HTML 文档。


如果在源代码中添加以特殊定界符 /** 开始的注释,你也可以很容易地生成一个看上去具有专业水准的文档。


4.9.1 注释的插入

javadoc 实用工具从下面几项中抽取信息:


模块

公共类与接口

公共的和受保护的字段

公共的和受保护的构造器及方法

注释放置在所描述特性的前面。注释以 /** 开始,并以 */ 结束


每个 /** ... */ 文档注释包含标记以及之后紧跟着的自由格式文本(free-form text)。标记以 @ 开始,如 @since 或 @param


自由格式文本的第一句应该是一个概要性的句子。javadoc 工具自动地将这些句子抽取出来生成概要页。


在自由格式文本中,可以使用 HTML 修饰符


要键入等宽代码,需要使用 {@code ...}


4.9.2 类注释

类注释必须放在 import 语句之后,类定义之前


4.9.3 方法注释

每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:


@param variable description 这个标记将给当前方法的 parameters 部分添加一个条目

@return description 这个标记将给当前方法添加 returns 部分

@throws class description 这个标记将添加一个注释,表示这个方法有可能抛出异常

4.9.4 字段注释

只需要对公共字段(通常指的是静态常量)建立文档


4.9.5 通用注释

标记 @since text 会建立一个 since 条目。text 可以是引入这个特性的版本的任何描述


下面的标记可以用在类文档注释中


@author name 这个标记将产生一个 author 条目

@version text 这个标记将产生一个 version 条目

通过 @see 和 @link 标记,可以使用超链接,链接到 javadoc 文档的相关部分或外部文档


标记 @see reference 将在 see also 部分增加一个超链接。它可以用于类中,也可以用于方法中


4.9.6 包注释

包注释,就是需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:


1.提供一个名为 package-info.java 的 Java 文件。这个文件必须包含一个初始化的以 /** 和 */ 界定的 Javadoc 注释,后面是一个 package 语句。它不能包含更多的代码或注释。


2.提供一个名为 package.html 的 HTML 文件。会抽取标记 ... 之间的所有文本


4.9.7 注释抽取

如果是一个包,应该运行命令


javadoc -d docDirectory nameOfPackage


如果要为多个包生成文档,运行:


javadoc -d docDirectory nameOfPackage1 nameOfPackage2


如果文件在无名的包中,就应该运行


javadoc -d docDirectory *.java


4.10 类设计技巧

1.一定要保证数据私有


绝对不要破坏封装性


2.一定要对数据进行初始化


Java 不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,可以提供默认值,也可以在所有构造器中设置默认值


3.不要在类中使用过多的基本类型


要用其他的类替换使用多个相关的基本类型。这样会使类更易于理解,也更易于修改


4.不是所有的字段都需要单独的字段访问器和字段更改器


在对象中,常常包含一些不希望别人获得或设置的实例字段


5.分解有过多职责的类


如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解


6.类名和方法名要能够体现它们的职责


类名应当是一个名词(Order),或者前面有形容词修饰的名词(RushOrder),或者是有动名词(有 -ing 后缀)修饰的名词(BillingAddress)。


访问器方法用小写 get 开头,更改器方法用小写的 set 开头


7.优先使用不可变的类


更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。


相关文章
|
6天前
|
JSON 程序员 C#
使用 C# 比较两个对象是否相等的7个方法总结
比较对象是编程中的一项基本技能,在实际业务中经常碰到,比如在ERP系统中,企业的信息非常重要,每一次更新,都需要比较记录更新前后企业的信息,直接比较通常只能告诉我们它们是否指向同一个内存地址,那我们应该怎么办呢?分享 7 个方法给你!
|
6月前
|
C++
c++类&对象
c++类&对象
50 3
|
6月前
|
存储 Java 编译器
类、对象、方法
摘要: 本文介绍了面向对象编程的概念,以京东购买手机为例,展示了如何通过分类和参数选择商品,强调软件与现实生活的对应关系。柯南三步走揭示了京东如何通过搜索和筛选帮助用户找到所需商品,而这一切背后的编程思想即为面向对象编程。面向对象编程涉及抽象、自定义类型和实例化对象等步骤,其中自定义类型(如Java中的类)用于封装现实生活中的复杂数据。文章还讲解了如何定义类、实例化对象以及访问权限修饰符、构造方法、this关键字、方法的使用,强调了方法参数和返回值在不同数据类型上的处理差异。整个讨论旨在阐明Java中面向对象编程的基本原理和实践应用。
44 5
|
6月前
深入类的方法
深入类的方法
|
6月前
|
存储 C#
C#对象和类
C#对象和类
45 0
|
6月前
|
存储 C++
C++对象和类
C++对象和类
38 0
|
6月前
|
存储 算法 Java
第 4 章 对象与类(上)
第 4 章 对象与类
79 0
|
Java 编译器
类 对象 封装
类 对象 封装
77 0
|
编译器 C语言 C++
C++ 类 & 对象
【摘要】 C++ 类 & 对象C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,通常被称为用户定义的类型。类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。类中的数据和方法称为类的成员。函数在一个类中被称为类的成员。C++ 类定义定义一个类,本质上是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的名称意味着什么,也就是...
|
设计模式 Python
我为什么要创建一个不能被实例化的类
我为什么要创建一个不能被实例化的类
72 0