Objects and Classes

《Java Core》.ed.11 学习笔记

介绍面向对象编程

OOP是当前主流的编程范式。OO的程序是由对象构成,每个对象有其特殊的功能,将其暴露给用户,且隐藏实现细节。只要一个对象满足你的要求,那么不必关心其功能如何实现

曾经,结构化编程是写好算法,然后存储数据,在数据上运行算法的过程。然后有了Niklaus Wirth的 Algorithm + Data structure = Programs,要注意,这里算法在先,而数据结构在后。这反映了当时的编程习惯,先要决定处理数据的程序,然后给数据设计结构,以使程序更容易实现。OO将这个过程倒过来,把数据(结构)放在前面,然后再设计在数据上的操作

如果处理一个小问题,那么两种编程范式解决问题效率差不多。但是在规模比较大的问题中(一个浏览器为例,过程式编程需要2000个方法/算法在全局数据上进行操作,而OO的实现可能是100个类,每个类)。其次,后者的结构中查找bug会更容易

一个类是对象被创建的模板或者蓝图。当从类中构造一个对象,即创建一个类的实例(instance of the class)

封装(Encapsulation或者信息隐藏)是对象的一个核心概念。正式地讲,封装只是将数据和行为结合到一块,并且将实现细节和用户隔离开。对象中的数据叫做它的实例属性(field),定义在这些数据上的操作叫作方法(method)。每个对象都有它自己的值,这些值就构成了这个对象的状态(state),当调用这个对象上的方法时,它的状态会改变

封装的核心在于除了类内部,其它地方不能直接访问一个对象的属性。程序要和对象数据交互,只能通过类暴露出的方法。封装给了对象黑盒的属性,这也是它重用和可靠的手段

在写自己的类时,OOP的另一个原则让这一切变得简单,即类可以通过继承(extend)别的类来构造。Java中有一个”cosmic superclass“(超级父类),即Object,所有其它的类都继承于它,然后在自己实现的类中扩展其它的属性和方法,新类具有它们继承的类的所有属性和方法,这个过程叫做继承(inheritance)

对象

要应用OOP,需要搞清楚对象的三个核心特征

  1. 对象的行为:用这个对象可以干嘛,或者说这个对象应该提供哪些方法
  2. 对象的状态:当调用这个对象的方法时,它如何响应
  3. 对象的身份:这个对象如何和其它可能跟他包含一样属性、方法的对象区分(唯一性)

一个对象状态改变必须是调用其方法后的结果(如果不是这样,那么封装的原则被破坏了)。一个类创建出的两个独立对象总是有不同的状态(state)和不同的身份(identity)

一个对象的状态可以影响它的行为(例:一个已经发货的订单不能再进行下单操作)

声明类

声明类的一个简单方法就是在问题分析中找名词。声明方法就是找动词

例:订单处理系统中

名词:Item / Order / Shipping address / Payment / Account
动词:add / remove

这只是一个常用的方式,经验会帮助你更好地找到在应用中重要的类名和方法名

类之间的关系

常见的类之间的关系包含

  • Depndence(“uses-a”):A使用了B(A依赖于B)
  • Aggregation(“has-a”):A包含B
  • Inheritance(“is-a”):A是B(的某一种)

依赖关系是最直观也最通用的关系。一个类如果要使用另一个类中的方法或者需要操作另一个类的对象,那么这个类依赖另一个类

要尽量最小化互相依赖的类的数量,因为如果类A不依赖类B,那么类B出现任何问题都不会影响到类A的功能(即尽量解耦)

包含关系中,一个类A的对象包含类B的对象

继承关系中,表示出来的是特殊类(special)和通用类(general)之间的关系。一般来说,如果类A继承类B,那么类A继承了类B的所有方法并且有更多功能

UML中的类图来表示应用中类之间的关系

使用预定义的类

对象和对象变量

要使用对象,首先要构造它们,并且指定它们的初始状态,然后使用这些对象上的方法。Java中,使用构造器来构造新的实例。一个构造器是一个特殊方法,目的是构建、初始化对象

Date类为例,它描述的是一个时间点

构造器名字和类名相同,new Date()创建了一个对象,它是使用当前的日期和时间初始化的。如果希望重复使用这个对象,那么通过Date birthday = new Date();来存下这个对象

对象和对象变量之间有一个重要的区别,Date deadline;,这个变量没有引用任何类型的对象,即一个对象变量不是一个对象,不能在它上面调用方法。deadline = new Date();初始化了变量,或者引用了其它现成的对象

最重要的一点,对象变量背后并不是一个对象,只是它引用了一个存储在别处的对象。new操作符的返回值也是一个引用

所有的Java对象都在堆(heap)中,当一个对象包含另一个对象变量,其实它只是包含了一个指向堆上另一个对象的指针(Java中的null对象上的操作会有运行时错误,而不会给出错误的内存地址)。Java中,必须使用clone方法才能得到一个对象的完整copy

LocalDate

Date类不擅长处理日历时间。Java库的设计者决定分开实现时间和指定名字的时间点。因此,标准库有两个类Date代表一个时间点,LocalDate类表示日历中的日期(Java8又新增了一些日期时间管理的类)

使用不同的类来表示不同的概念是一个好习惯。使用LocalDate的静态工厂方法来调用构造器创建对象LocalDate.now(),表示了创建对象的日期。LocalDate.of(1999, 12, 31)创建特定的某一天

// 有了LocalDate对象后,可以获取其它信息
LocalDate newYearsEve = LocalDate.of(1999, 12, 31);
int year = newYearsEve.getYear(); // 1999
int month = newYearsEve.getMonthValue(); // 12
int day = newYearsEve.getDayOfMonth(); // 31

// 之后的日期
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
year = aThousandDaysLater.getYear(); // 2002
month = aThousandDaysLater.getMonthValue(); // 09
day = aThousandDaysLater.getDayOfMonth(); // 26

Mutator and Accessor Methods

LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
// 在调用了方法之后,newYearsEve对象发生了什么变化?它是否改为了1000天之后?实际上这个方法产生了一个新的LocalDate。原始对象并没有改变
// 这种情况下,称plusDays没有改变(mutate)原对象
// 早期Java的GregorianCalendar类是处理日历的
GregorianCalendar someDay = new GregorianCalendar(1999, 11, 31);
someDay.add(Calendar.DAY_OF_MONTH, 1000);
// add方法是一个mutator方法,在调用之后,原对象的状态发生了改变
year = someDay.get(Calendar.YEAR); // 2002
month = someDay.get(Calendar.MONTH) + 1; // 09
day = someDay.get(Calendar.DAY_OF_MONTH); // 26

那些只访问对象而不修改它们的方法有时被称为访问方法(accessor method)

定义自己的类

An Employee Class

// 最简单的类定义的形式
class ClassName
{
    field1
    field2
    . . .
    constructor1
    constructor2
    . . .
    method1
    method2
    . . .
}

class Employee
{
    // instance fields
    private String name;
    private double salary;
    private LocalDate hireDay;
    // constructor
    public Employee(String n, double s, int year, int month, int day)
    {
        name = n;
        salary = s;
        hireDay = LocalDate.of(year, month, day);
    }
    // a method
    public String getName()
    {
        return name;
    }
    // more methods
    . . .
}

Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", . . .);
staff[1] = new Employee("Harry Hacker", . . .);
staff[2] = new Employee("Tony Tester", . . .);

for (Employee e : staff)
    // 每个职员的薪水上涨5%
    e.raiseSalary(5);
    
for (Employee e : staff)
    System.out.println("name=" + e.getName()
        + ",salary=" + e.getSalary() + ",hireDay=" + e.getHireDay());

本例中的源文件名叫做EmployeeTest.java,因为文件名要匹配公有类的名字。一个源文件中只能有一个公有类,但是可以有任意多个非公有类。编译源码时,会产生两个.class文件,通过java EmployeeTest来调用程序

多个源文件的使用

所有的源文件如果可以匹配通配符,那么可以被一起编译成.class文件。例如javac Employee*.java。实际上,当Java编译器看到一个类被另外一个类使用了,那么它会找到这个类的字节码文件,如果找不到,就会找同名的.java文件,然后编译它。此外,如果这个类的修改时间比.class文件更新,那么它会重新编译源文件

解剖Employee类

// Employee类中的方法
public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)
// Employe类中的属性
private String name;
private double salary;
private LocalDate hireDay;

public表示在任何类中都可以调用这些方法。private保证唯一能访问这些属性的方法只能是类自身的方法

将字段设置为public不是一个好主意,公有属性表示任何类的任何方法可以直接去修改这个字段。强烈建议将字段都设为私有

首先使用构造器

public Employee(String n, double s, int year, int month, int day)
{
    name = n;
    salary = s;
    hireDay = LocalDate.of(year, month, day);
}

构造器在你通过给定实例属性以创建对象的时候运行,即new Employee("James Bond", 100000, 1950, 1, 1)就创建了一个实例。构造器和其它方法的重要区别是,构造器只能结合new操作符被调用

关于构造器的几个要点:

  1. 和类同名
  2. 可以有一个及以上构造器
  3. 可以有0个、1个或多个参数
  4. 没有返回值
  5. 总是使用new操作符调用

要注意在构造器中不要使用和实例属性同名的局部变量,这些变量只能在构造器内使用,且会覆盖实例属性

使用var声明局部变量

Java10中,可以使用var来声明局部变量,不用指定它们的类型,它们的类型可以通过初始值的类型来推断

// 等价
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);

var关键字只能在方法内部的局部变量上使用

(类型推断是好,可惜1.8没有啊)

null引用

null值表示缺少对象。如果在null上调用了方法,那么会出现NPE(NullPointerException)(和数组越界一样的老问题),程序不会“捕获”异常,而是直接终止。在定义类时,直到哪个属性值会为null是个好主意(基本类型不会为null--都有默认值)

解决这个问题有两种方法:

  1. 保证代码
if (n == null) 
    name = "unknown"; 
else 
    name = n;
  1. Java9之后,有更简单的方法
public Employee(String n, double s, int year, int month, int day)
{
    name = Objects.requireNonNullElse(n, "unknown");
    . . .
}

public Employee(String n, double s, int year, int month, int day)
{
    // “tough love”方式(不太懂)
    // 如果使用null的name构造对象,会抛出一个NPE
    // 抛出异常可以描述错误发生地
    // 抛出异常有错误描述
    Objects.requireNonNull(n, "The name cannot be null");
    name = n;
    . . .
}

Implicit and Explicit Parameters

对象上的方法访问它们的实例属性

public void raiseSalary(double byPercent)
{
    double raise = salary * byPercent / 100;
    salary += raise;
}

number007.raiseSalary(5);

raiseSalary方法有两个参数,第一个参数叫做implicit parameter,是Employee类型的对象(即调用这个方法的对象),第二个参数,在括号内的叫做explicit parameter

在每个方法中,this关键字引用的就是隐式参数,即

public void raiseSalary(double byPercent)
{
    double raise = this.salary * byPercent / 100;
    this.salary += raise;
}

JIT(just-in-time)编译器会查看所有的短的、常被使用的、没有被覆写的方法,对它们进行优化

封装的好处

public String getName()
{
    return name;
}
public double getSalary()
{
    return salary;
}
public LocalDate getHireDay()
{
    return hireDay;
}

当你想获取或改变实例属性的时候,可以通过

  1. 私有数据属性
  2. 共有方法getter
  3. 共有方法setter

这些冗余代码的好处

  1. 只要方法名不变,其余的属性改变对外不可见
  2. Mutator方法可以进行代码检查(是否允许修改)

注意:尽量不要给mutable对象的引用写访问方法(getter)

class Employee
{
    private Date hireDay;
    . . .
    public Date getHireDay()
    {
        return hireDay; // BAD
    }
    . . .
}

如前所述,Date类的方法setTime是mutator方法,会修改对象状态

Employee harry = . . .;
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
// let's give Harry ten years of added seniority
d.setTime(d.getTime() - (long) tenYearsInMilliSeconds);

d harry.hireDay指向的其实是一个对象,所以调用dsetTime方法会改变对象的状态。如果要返回可变对象的引用,那么需要先clone它

class Employee
{
    . . .
    public Date getHireDay()
    {
        return (Date) hireDay.clone(); // OK
    }
    . . .
}

基于类的访问权限

一个类中的方法被允许访问任何同类型的任何对象的私有属性

私有方法

私有方法一般是在类内部拆分逻辑时使用,例如helper方法,这些方法不能作为用户使用(被调用)的接口。当一个方法被声明为private,那么他要保证这个方法在别的地方不会被用到。一旦方法被声明为public,那么这个方法不能被随便删除

final 实例属性

可以将实例属性定义为final,这种属性在对象被创建时必须被初始化。final修饰符对于类型是基本类型和immutable类(类内所有方法不能修改对象状态,例如String)的属性是很有用的。给可变对象属性定义成final会导致歧义(虽然不能引用新对象,但是它本身是可变的,即不是“final”的)

静态属性和方法

静态属性

如果将一个属性定义为static,那么每个类都只有这一个属性。相反,每个对象都有它自己的非静态实例属性

// 每个对象都有自己id属性,但是nextId属性是被所有实例共享的属性
// 即使一个Employee对象都不存在,静态属性也是存在的,它属于类,而不是某个单独对象
class Employee
{
    private static int nextId = 1;
    private int id;
    . . .
}

静态常量

静态变量比较少,而静态常量很常见

public class Math
{
    . . .
    public static final double PI = 3.14159265358979323846;
    . . .
}

public class System
{
    . . .
    public static final PrintStream out = . . .;
    . . .
}

公有的常量是很有用的(易用)

System类有一个setOut方法,可以修改final变量,但是这个方法是一个native方法,不是Java语言实现的。在自己的程序中应该不会出现这种情况

静态方法

不在对象上操作的方法是静态方法,Math.pow(x, a),它不使用Math对象来实现功能,它也没有隐式参数。静态方法不能访问实例属性,因为它不能在对象上操作。然而,静态方法可以访问静态属性

使用对象来调用静态方法是合法的,然而,这种用法有歧义(静态方法一般不会操作实例属性,即和创建出来的对象没关系)。建议使用类名调用(阿里开发手册说明这种调法影响性能)

两种情况下使用静态方法

  1. 方法不访问对象的状态(各种属性),因为所有的显式参数就可以完成功能
  2. 方法只需要访问类的静态属性

工厂方法

静态方法的另一种常用场景是工厂方法。例如LocalDate NumberFormat,它们使用静态工厂方法来创建对象,LocalDate.now LocalDate.of,再举一个NumberFormat的例子

NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // prints $0.10
System.out.println(percentFormatter.format(x)); // prints 10%

为什么不使用构造器?

  1. 构造器不能命名(希望使用不同的名字来获取不同的NumberFormater)
  2. 构造器返回的数据类型一定是构造器的数据类型,而工厂方法可以返回子类对象(DecimalFormat

main方法

在没有对象的条件下可以调用静态方法。因此,main方法是静态方法。main方法操作任何对象,实际上,程序开始时,没有任何对象。静态main方法执行,创建程序需要的对象

每个类都可以有一个main方法,这是一个常用的类的单元测试方法

方法参数

按值调用:方法得到的时调用者提供的值
按引用调用:方法得到的是调用者提供这个变量的地址

因此,一个方法可以修改按引用调用传进来的值,而不能改变按值调用传的值。Java总是按值调用,这代表方法得到的是所有参数值的复制。所以方法不能改变任何传入的参数变量

// 调用之后 percent还是10
double percent = 10;
harry.raiseSalary(percent);

public static void tripleValue(double x) // doesn't work
{
    x = 3 * x;
}

上述程序的解释;

  1. x是percent的值的复制来初始化的
  2. x变成x的3倍,30.但是percent还是10
  3. 方法结束后,参数x不会再使用

有两种方法参数类型:

  1. 基本类型(primitive type)
  2. 对象引用(object references)

即在Java中,方法不能改变基本类型的参数的值

public static void tripleSalary(Employee x) // works
{
    x.raiseSalary(200);
}

harry = new Employee(. . .);
tripleSalary(harry);

上述代码的过程:

  1. x通过harry的值的复制来初始化(一个对象的引用)
  2. raiseSalary方法应用到了对象引用,x和harry引用的对象的salary都上涨了
  3. 方法结束,x不再使用,harry还在,但是这个对象的salary还是上涨了

Java对于对象并不是按引用调用

public static void swap(Employee x, Employee y) // doesn't work
{
    Employee temp = x;
    x = y;
    y = temp;
}

如果Java使用按引用调用,那么这个方法会奏效(x指向y,y指向x)

var a = new Employee("Alice", . . .);
var b = new Employee("Bob", . . .);
// does a now refer to Bob, b to Alice?
swap(a, b);

然而这个方法不能改变存在a和b中的对象引用。x和y参数是用它们的复制来初始化的,swap操作的也都是复制上的。方法结束后,x和y被丢弃,原始的a和b还各自引用原来的对象

Java中:

  1. 一个方法不能修改基本数据类型的参数
  2. 一个方法可以改变对象参数的状态
  3. 一个方法不能让对象参数指向一个新对象

对象创建

Overloading

有些类有超过一个的构造器,例如

var messages = new StringBuilder();
var todoList = new StringBuilder("To do:\n");

这种能力叫做重载(overload),如果一些方法同名,但是参数不同,那么编译器会根据参数决定调用哪个方法。如果编译器不能匹配参数或者匹配不到任何一个方法或者没有唯一一个比别的方法更适合的方法会报编译器错误(这个过程叫做overloading resolution)

方法名+参数类型=方法签名。返回值类型不是方法签名的一部分。所以不能有相同方法签名但是返回值类型不同的两个方法

默认属性初始化

如果不显式在构造器中设置属性值,它会自动设为默认值。数字为0,布尔值为false,对象引用为null。当然,使用默认值不是一个好的实践。局部变量和实例属性有很大的区别,局部变量必须被初始化,而类中的属性如果没有初始化会被默认值初始化

没有参数的构造器

很多类包含一个没有参数的构造器,创建一个属性设为恰当的默认值的对象

public Employee()
{
    name = "";
    salary = 0;
    hireDay = LocalDate.now();
}

如果一个类提供了至少一个构造器但没有提供无参构造器,那么不提供参数来创建对象就是不合法的。如果类中没有其它构造器,那么会有一个默认(free)的无参构造器,只要你提供了一个构造器,那么再想拥有无参构造器就需要自己定义

显式属性初始化

class Employee
{
    private String name = "";
    . . .
}

赋值语句在构造器执行之前执行。如果所有的构造器都会将某个属性设为某个值,那么使用这种方式比较方便。被初始化的属性不一定是常量

参数名

构造器的参数名的起名形式:

  1. 每个参数前加一个a
  2. 和属性名相同,通过this来访问属性

调用另一个构造器

this引用的是一个方法的隐式参数,它还有第二种含义。如果构造器的第一行语句是this(. . .),那么这个构造器调用了同一个类中的其它构造器

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

初始化块

之前已经有两种初始化属性的方式:构造器指定值和声明时给定值。Java中还有第三种方式,叫做初始化块。类的声明中可以包含任意代码块,当这个类的对象被创建时,代码块执行

class Employee
{
    private static int nextId;
    private int id;
    private String name;
    private double salary;
    // object initialization block
    {
        id = nextId;
        nextId++;
    }
    public Employee(String n, double s)
    {
        name = n;
        salary = s;
    }
    public Employee()
    {
        name = "";
        salary = 0;
    }
    . . .
}

无论哪个构造器被调用,代码块中的代码一定先执行,然后是构造器的代码执行。这个机制不是必要的也不常用。即直接用构造器更直观。代码块中设置定义在其后的值是合法的,但是读取是不合法的(Spec中这里的规则很难,所以建议把代码块放到属性定义后面)

当一个构造器被调用:

  1. 如果构造器的第一行调用了第二个构造器,那么第二个构造器执行
  2. 否则
    1. 所有属性用它们的默认值初始化
    2. 所有属性初始化器(initializers)和初始化块被执行,顺序是它们出现在类定义中的顺序
  3. 构造器执行

为了初始化一个静态属性,提供初始值或者使用静态初始化块都可以。如果类中的静态属性初始化过程很麻烦,那么使用静态初始化块

static
{
    var generator = new Random();
    nextId = generator.nextInt(10000);
}

当类第一次被加载,静态资源进行初始化。静态属性初始化(initializers)和静态代码块的执行顺序和它们的出现顺序相同

直到JDK6,使用静态代码块来实现不写main输出“Hello World”都可以实现。Java7开始,java程序会先检查是否有main方法

对象销毁和finalize方法

C++中,当一个对象不被使用,那么必须显式定义析构器(destructor)。析构函数的常见行为就是将内存清空。因为Java有GC,所以Java也不支持析构函数

有些对象会使用除了内存以外的资源,例如文件或者其它用户系统的资源。此时在使用完成之后将该资源返还、回收是很重要的。可以通过调用它们的close()方法

如果可以等待VM退出,那么添加一个钩子方法(shutdown hook)(通过Runtime.addShutdownHook方法),Java9中,可以使用Cleaner类来注册方法(当一个对象不再被访问时要做的操作)。实际上这都不是常用手段

不要使用finalize方法来清理资源,这个方法是为了在GC工作之前调用。然而,用户(开发者)是不知道什么时候该被调用,而且这个方法已经被声明为过时的

Java允许将所有的类组合进一个集合,叫做包

包名

使用包主要是为了保证类名是唯一的。同名类放到不同包下是不会冲突的。其实,为了保证包名的唯一性,应使用Internet域名(唯一)的倒序。然后在之后加上项目名。在Java编译器中,包和它的嵌套包没有任何关系,java.util java.util.jar不会互相干扰

类的引入

一个类可以使用同包内所有的类和其它包中所有的公有类。使用其它包中的公有类有两种方式

  1. 使用全称包名 java.time.LocalDate today = java.time.LocalDate.now();
  2. 简单且常用的方法是使用import语句
    1. 引用包中的某个类 import java.time.LocalDate;
    2. 引用包中的所有类 import java.time.*

直接引入包中的所有类,并没有什么副作用,但是显式提供引入的类信息,可以提高可读性

只有当出现类名冲突时才会关注包的导入

import java.util.*;
import java.sql.*;

Date today; // ERROR--java.util.Date or java.sql.Date?

// 特殊import语句解决问题
import java.util.*;
import java.sql.*;
import java.util.Date;
// 如果两个Date都需要,那么使用全名
var deadline = new java.util.Date();
var today = new java.sql.Date(. . .);

定位包中的类是编译器的工作。class文件中的字节码是使用全包名来引用其它类的

#includeimport完全不一样。C++中,必须使用#include来包含外部特性的声明,因为C++编译器不看文件内部,除非它正在编译这个文件和它外部包含的头文件。Java编译器如果你告诉它在哪里看文件,它会去看其它的类文件

C++中,和包机制类似的结构是namespace

Static Imports

import语句可以引入静态方法和属性,而不只是类

// 可以不加类名使用静态方法和属性
import static java.lang.System.*;
import static java.lang.System.out;

静态引入可以让代码变得更简洁(老忘了用)

Addition of a Class into a Package

要将类放入包中,将包名放到源文件的第一行

package com.horstmann.corejava;
public class Employee
{
    . . .
}

所有在com.horstmann.corejava包中的源文件,都应该在com\horstmann\corejava文件夹下

编译器在编译源文件的时候不会检查目录结构。假如编译一个源文件,它不在它声明的包对应的子目录下,如果它不依赖其它的类,那么编译不会报错,但是运行会报错,除非它被放到了正确的地方。如果包不匹配目录,那么虚拟机无法找到类文件

Package Access

如果在类上面不指定public private,那么这个类的特性只能被同包下的所有方法。对于类来说,这个所谓的默认访问限制可以理解,但是类中的属性必须被显式声明为private,否则它也会有默认的访问机制(破坏了封装)

类路径

类被存储在文件系统的子目录中。类的路径必须要匹配包名。class文件也可以存在JAR文件中,一个JAR包含多个类文件和压缩模式的子目录,节约空间并且改善性能。在程序中使用第三方库,经常会使用一个或多个JAR文件

JAR文件使用ZIP格式来组织文件和目录。可以使用ZIP工具来查看JAR的目录结构。要在程序中共享类,需要按如下步骤:

  1. 将类文件放到目录中,例如/home/user/classdir,这就是包树的根目录,如果添加了类com.horstmann.corejava.Employee,那么它必须在/home/user/classdir/com/horstmann/corejava这个位置
  2. 将JAR放到目录中,例如/home/user/archives
  3. 设置类路径。UNIX中(/home/user/classdir:.:/home/user/archives/archive.jar)Windows中(c:\classdir;.;c:\archives\archive.jar)

Java6开始,可以使用通配符来指定JAR文件目录。例如/home/user/classdir:.:/home/user/archives/'*' c:\classdir;.;c:\archives\*。Unix中,通配符必须加引号。在归档目录(库)中的JAR文件被包含在类路径中

不要专门将JavaAPI加入到类路径中,它总能被找到

javac编译器总是在当前目录查找文件,如果“.“目录在类路径中,Java虚拟机launcher只会查看当前目录。如果没有设置类路径,默认的类路径包含”.“目录,编译没有问题,但是不能运行

/home/user/classdir:.:/home/user/archives/archive.jar,如果虚拟机要寻找com.horstmann.corejava.Employee,它首先在JavaAPI类中看,找不到类文件,然后会看类路径

然后它会查看以下文件:

  • /home/user/classdir/com/horstmann/corejava/Employee.class
  • 当前目录的com/horstmann/corejava/Employee.class
  • /home/user/archives/archive.jarcom/horstmann/corejava/Employee.class

编译器定位文件比虚拟机更多。如果在不指定包的情况下引用了一个类,那么编译器要找包含这个类的包,它将所有已知目录当作这个类的可能来源

如果超过一个类被找到,那么会报编译器错误(即不指定包名直接用了两个包的同名类)。编译器还做了一件事,它会查看源文件是否比class文件更新,如果是,那么源文件会被重新编译

只能从其它包引入公共类,源文件只能包含一个公共类,源文件要和公共类同名。因此,编译器容易定位公共类所在文件。然而同包内引用非公共类,这些类可以和源文件名不同。如果从当前包引入了一个类,那么编译器会查找当前包下所有的源文件

设置类路径

使用-classpath -cp Java9 -> --class-path

java -classpath /home/user/classdir:.:/home/user/archives/archive.jar
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg

长命令最好放到一个shell脚本或batch文件中执行

另一种方式是CLASSPATH环境变量的设置,细节依赖于系统的Shell

bash中

export CLASSPATH=/home/user/classdir:.:/home/user/archives/archive.jar

windows shell中

set CLASSPATH=c:\classdir;.;c:\archives\archive.jar

在shell存在的时候,类路径都有效

有些人建议将类路径设为永久变量。通常来说不是一个好主意,人们很容易忘记全局设置,这有可能导致类加载失败(例:Apple’s QuickTime的windows安装器,它全局设置CLASSPATH来指向它需要的JAR包,但是不包括当前目录,结果是,无数Java程序员在程序编译成功但是运行失败后找不到原因)

过去,一些人建议统一类路径,将所有的JAR放到jre/lib/ext目录,当早就忘了的类被加载出来会造成困扰

Java9之后,类可以从模块路径被加载

JAR文件

当你打包应用时,给到用户的最好是单个文件,而不是一个包含着class文件的目录。JAR的设计就是为了实现这个目的。JAR可以包含类文件和其它的文件类型(图片、音频),此外JAR时使用ZIP压缩格式制作的

创建一个JAR

使用jar工具来制作JAR文件(JDK的jdk/bin中),最常用的命令是

jar cvf jarFileName file1 file2 . . .
jar cvf CalculatorClasses.jar *.class icon.gif
// 一般格式
jar options file1 file2 . . .

命令行参数就不记录了,用时再查

可以将应用程序跟代码库打包进JAR文件

The Manifest

除了类文件和其它类型文件,每个JAR文件包含一个manifest文件,它描述了这个压缩包的特性。这个文件叫做MANIFEST.MF,它放在JAR文件的META-INF子目录下。复杂的manifest有很多内容(entries),它们被组合进不同的小结。第一个小结叫做main section,这些内容会应用在整个JAR文件。后面的小结可以指定命名的entries的属性(文件、包、URL),这些entries必须以Name entry,小结通过空行分隔

Manifest-Version: 1.0
lines describing this archive
Name: Woozle.class
lines describing this file
Name: com/mycompany/mypkg/
lines describing this package

要编辑manifest,将lines的内容通过text文件的形式插入到文件中,然后

jar cfm jarFileName manifestFileName . . .
// 根据给定manifest来创建一个jar
jar cfm MyArchive.jar manifest.mf com/mycompany/mypkg/*.class
// 更新一个已存在的JAR文件的manifest
jar ufm MyArchive.jar manifest-additions.mf

了解更多

执行JAR文件

可以通过jar命令的选项来指定程序的入口(程序开始运行的类)

jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass

此外,也可以在manifest中指定哪个类为入口

Main-Class: com.mycompany.mypkg.MainAppClass

manifest必须以新行结尾。否则,没办法被正确读取

无论使用哪种方法,用户只需要java -jar MyProgram.jar即可开始运行程序。根据一些操作系统的配置,用户可能可以双击运行JAR文件

Multi-Release JAR Files

随着模块的变化和包的封装更好,之前可以访问的内部API可能不再能使用(Java8和Java9的过程中API发生的变化)。Java9推出了multi-release JARS,它可以包含不同的Java发行版的class文件

为了向后兼容,额外的类文件被放在META-INF/versions目录下

Application.class
BuildingBlocks.class
Util.class
META-INF
├─ MANIFEST.MF (with line Multi-Release: true)
├─ versions
├─ 9
│ ├─ Application.class
│ └─ BuildingBlocks.class
└─ 10
└─ BuildingBlocks.class

不同的Java版本使用不同的类。Java8不知道META-INF/versions中的内容

要添加版本的类文件,使用--release标志

jar uf MyProgram.jar --release 9 Application.class

要构建一个multi-release的JAR文件,使用

jar cf MyProgram.jar -C bin/8 . --release 9 -C bin/9 Application.class

在编译不同的发行版本类时,使用--release -d

javac -d bin/8 --release 8 . . .

在Java9中,-d创建目录(如果不存在) --release标志也是Java9推出,之前需要-source, -target, -bootclasspath

multi-release JAR的目的是让你的程序或库的某个版本可以和多个JDK版本兼容

命令行参数的一些解释

不记录了,真用到的话再看吧

文档注释

JDK包括非常有用的工具,javadoc,它可以从源文件生成HTML文档,事实上,在线API文档就是在源码上运行javadoc程序

如果在源码上添加/** */注释,就可以生成专业文档。这可以使代码和文档保持统一

插入注释

javadoc从以下项目中抽取信息

  • Modules
  • Packages
  • Public classes and interfaces
  • Public and protected fields
  • Public and protected constructors and methods

对上述所有内容都可以加注释。每个注释都放在被描述对象的上面。每个/** ... */注释都包含任意形式的文本(跟在tag后,tag以@开头),例如@since @param

文本的第一句应该使总结句

javadoc工具自动生成总结页,包含所有的总结句。在文本中可以使用HTML修饰符

<em>. . .</em> 重点
<strong>. . .</strong> 粗重点
<ul>/<li> 无序列表
<img . . ./> 图片
{@code . . . }表示代码而不用 <code>. . .</code>

且不需要顾虑转义 <

如果注释包含其它文件的链接,将这些文件放入子目录,叫做doc-files,javadoc会复制这个目录的内容到文档目录 例(<img src="doc-files/uml.png"alt="UML diagram"/>

类注释

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

/**
* A {@code Card} object represents a playing card, such
* as "Queen of Hearts". A card has a suit (Diamond, Heart,
* Spade or Club) and a value (1 = Ace, 2 . . . 10, 11 = Jack,
* 12 = Queen, 13 = King)
*/
public class Card
{
    . . .
}

注释中的*号不是必要的

方法注释

方法注释必须放在它描述的方法前。除了常用tag外,还可以使用@param描述变量,@return描述返回值 @throws描述抛出的异常

/**
* Raises the salary of an employee.
* @param byPercent the percentage by which to raise the salary (e.g., 10 means 10%)
* @return the amount of the raise
*/
public double raiseSalary(double byPercent)
{
    double raise = salary * byPercent / 100;
    salary += raise;
    return raise;
}

属性注释

只需要给共有属性添加文档注释

/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;

通用注释

@since 从哪个版本开始具有此特性

类注释可以用

@author name 可以有多个作者tag
@version text 版本信息

可以使用超链接来看其它的章节和外部文档

@see reference 添加了一个超链接在(see also小结)。它可以让类和方法使用,这里_reference_可以是以下几种之一

package.class#feature label
<a href=". . .">label</a>
"text"

第一种最常用:@see com.horstmann.corejava.Employee#raiseSalary(double),可以看到这个方法,可以忽略包名和类名,此时这个方法会被定位在当前包或当前类,必须使用#来分隔类名和方法名
如果@see tag后有<,那么需要提供一个超链接 @see <a href="www.horstmann.com/corejava.html">The Core Java homepage</a>,在链接中可以改变label来当锚点
如果@see tag后有",那么提供一段文本 @see "Core Java 2 volume 2"

可以在一个特性中增加多个@see 标志,但是必须把它们放在一起

将链接到其它的类或方法的超链接放在文档注释的任何地方 {@link package.class#feature label}

Java9中,使用{@index entry}tag增加搜索框entry

包注释

每个包要单独添加一个文件来写包的文档注释,两种方式

  1. 提供一个java文件,叫package-info.java。这个文件只包括Javadoc注释,不能有其它代码
  2. 提供一个html文件,叫package.html,所有的<body>中内容被当作文档注释

注释抽取(生成)

  1. 将目录移到包含源文件的位置
  2. 运行命令
// 一个包
javadoc -d docDirectory nameOfPackage
// 多个包
javadoc -d docDirectory nameOfPackage1 nameOfPackage2. . .
// 无名包
javadoc -d docDirectory *.java

如果缺少-d参数,HTML文件会被提取到当前目录

JavaDoc工具

类的设计原则

  1. 保持数据私有
  2. 总是初始化数据
  3. 不使用太多的基本类型(组织数据而不是分散数据)
  4. 不是所有属性都需要单独的属性访问方法和改变方法(getter setter)
  5. 将做事情很多的类做拆分
  6. 类和方法的命名要见名知意
  7. 不可变的类(属性不可变)更好
posted on 2020-12-04 16:50  老鼠不上树  阅读(143)  评论(0)    收藏  举报