面向对象编程(OOP)

  • Java中对象变量只是包含了一个引用, 没有实际包含一个变量
1
2
3
Date startTime = new Date();
// 这里的startTime只是一个指向Date实例的引用
// 不能看作是CPP中的引用, 应该看作CPP中的对象指针, 也就是Date* startTime
  • 所有的Java对象都存储在堆中, 当一个对象包含了另一个对象的时候, 实际上只是包含了另一个对象在堆中的指针
  • 所以如果需要得到一个对象的副本, 不能简单的用=, 而是应该用clone()方法

更改器方法和访问器方法

更改器方法: 调用方法以后, 对应实例的状态会改变

访问器方法: 调用方法以后, 只访问对象, 不会修改它. 比如get()

  • 访问器方法不要返回可变对象的引用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class E {
    private Date hireDay;
    public Date getHireDay() {
    return hireDay;
    }
    }
    // 这里的hireDay就是一个Date类的对象引用, 而Date类本身存在更改器方法setTime
    // 所以此时hireDay是可变的, 破坏了封装性
    // 如果需要返回一个可变对象引用, 需要先clone
    class R {
    public Date getHireDay() {
    return (Date) hireDay.clone();
    }
    }
  • CPP中带有const后缀的方法是访问器方法, 没有const后缀的方法是更改器方法
  • Java中没有这种明显的标识
  • 构造器没有返回值, 总是和new一起使用
  • 所有的方法中都不要使用和实例字段同名的方法, 可以同名, 但是最好不要出现, 除了后面讲到的record
  • 实例字段不要设置成public, 这样会破坏封装, 要保证数据私有
  • 在构造类的实例时, 推荐使用var来声明这个对象的类型, 这样就不用重复写类了
  • 不要对数值类型写var, var只能用于局部字段, 对于参数和实例字段不能使用var

null

  • 对象变量包含一个引用, 或者是null
  • 如果对null值变量调用方法会产生NullPointException
  • 基本数据类型不会是null, 需要注意String类型的null, 可以检测到以后将其转换为另一个值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if (n == null) {
    name = "unknown";
    } else {
    name = n;
    }
    // 也有更简单的方法
    name = Objects.requireNonNullElse(n, "unknown");
    // 或者可以直接拒绝null
    name = Objects.requireNonNull(n, "The name can not be null");
    // 这样可以直接定位到哪里有空值, 如果等程序自动触发NPE的话, 可能不是空值存在的地方

静态字段

静态变量

  • 一个对象定义为static, 那么这个字段并不会出现在每个类的对象中. 每个静态字段只有一个副本
  • 所以静态字段属于类, 但是不属于单个类的实例

比如对员工分配唯一的标识码

1
2
3
4
5
6
7
8
9
10
class E {
private static int nextId = 1;
private int id;
public E() {
id = nextId;
nextId ++;
}
}
// 这样所有员工共享一个nextId, 但是每个员工有自己的id
// 就算没有员工对象, 这个nextId也是存在的

静态常量

  • 静态常量相比于静态变量更加常用
1
2
3
4
5
6
7
8
9
public class Math {
public static final double PI = 3.14...;
}

public class System {
public static final PrintStream out = ...;
}
// 分别使用Math.PI和System.out可以访问
// 如果省略了static, 那么PI就需要通过一个MATH的实例来访问
  • 类中最好不要有公共字段, 因为公共字段谁都可以访问, 但是如果是final的公共常量就不要紧
  • 因为out是由final修饰的, 所以out本身是不允许重新赋值的
  • 但是System中有一个setOut方法, 这是因为setOut方法不是Java实现的, 是一个原生方法, 可以跳过访问控制机制

静态方法

  • 静态方法是不操作对象的方法, 比如Math.pow(x, a), 不需要使用Math对象, 没有隐式参数this
  • 所以上面的E类, 静态方法不能访问id字段, 因为不操作对象, 但是静态方法可以访问静态字段
  • 同样可以使用对象实例调用静态方法, 但是没有意义, 因为静态方法与对象无关, 所以最好直接用类名调用静态方法

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

  1. 方法不需要访问对象状态, 所有的参数可以直接通过显示参数提供, 比如Math.pow(x, a)
  2. 方法只需要访问静态字段
  • main方法就是一个静态方法, 可以在每个类都创建一个静态方法, 用于演示
  • 不演示的时候直接调用Application.main则不会执行内部其他类的main函数

静态工厂方法

为什么不用构造器要用静态工厂方法:

  1. 无法为构造器命名, 因为构造器的命名总是要与类名相同, 但是如果需要得到两个不同的名字, 就无法实现了
  2. 构造器无法改变构造对象的类型, 静态工厂方法可以返回指定的类型, 比如某个类的子类

构造器

  • 可以在一个构造器中调用另一个构造器
    1
    2
    3
    4
    5
    6
    7
    public class E {
    public E() {
    // 调用了E(String, double)的构造方法, 这样只需要写一次公共的构造函数
    this("123" + nextId, s);
    nextId ++;
    }
    }
  • 自动定义的, 设置所有实例字段的构造器是标准构造器
  • 自定义构造器的第一个语句必须调用另一个构造器, 最终调用标准构造器

记录

JDK 14引入, 状态不可变, 公共可读, 一个记录的实例字段称为组件

1
2
3
4
5
6
7
record Point(double x, double y) {};
// 这个Point就是一个记录, 不再需要写class中的很多内容
// 具有一个构造器 Point(double x, double y)
// 具有两个访问器 public double x(); public double y();
// 方法和实例字段可以同名
var p = new Point(3, 4);
System.out.Println(p.x() + " " + p.y());
  • 每个记录都有自动定义的三个方法: toString(), equals(), hashCode()
  • 记录可以自己定义静态字段和方法, 但是不能新增实例字段, 实例字段应该全部都作为参数

包名为了确保类名的唯一性, 一般是域名的逆序.项目名.类名

  • 一个类可以使用所属包中的所有类, 以及其他包中的公共类
  • 如果包名写错了, 但是他不依赖其他包, 那么可以顺利编译通过, 但是执行的时候会失败, 因为虚拟机无法根据包名找到类

jar

  • 使用jar cvf jarFileName file1 file2 ... 创建新的jar文件, u选项可以更新jar
  • 每个jar都包含一个清单文件manifest用于描述归档文件的特殊性
  • 清单文件MANIFEST.MF位于jar文件的META-INF子目录中
  • 清单文件中包含多个条目, 分组成多个节, 第一节称为主节, 作用于整个JAR文件
  • 节与节之间使用空行分割, 除主节外,随后的每一节中的条目可以指定命名实体的属性, 比如单个文件, 包或者url, 都需要以Name条目开始
  • 如果需要编辑清单文件, 可以将需要添加到清单文件的行放到文本文件中, 使用jar cfm jarFileName manifestFileName
  • 如果需要更新清单文件, 可以将增加的部分放到文本文件中, 使用jar ufm xxx.jar manifest-additions.mf
  • 清单文件的最后一行需要以换行符作为结束, 否则无法正确读取
  • 可以使用jar cvfe xxx.jar xxxxxClass来指定程序的入口点
    • 或者可以在清单文件中添加主类Main-Class: xxxxxClass
    • 这样就可以使用java -jar xxx.jar来启动程序
      1
      2
      3
      4
      5
      6
      // Hello.java
      public class Hello {
      public static void main(String[] args) {
      System.out.Println("hello");
      }
      }
  • 比如上述Hello.java, 使用javac Hello.java可以编译为Hello.class, 运行java Hello可以直接输出"Hello"
  • 此时如果使用jar cvf Hello.jar Hello.java Hello.class, 将会生成Hello.jar
  • 运行java -jar Hello.jar, 会报错, 提示没有主清单属性
    • 第一种解决办法: 重新生成一个jar包, 生成的时候指定入口程序
      • jar -cvfe Hello.jar Hello Hello.java Hello.class
      • 第一个Hello.jar是生成的jar包, 第二个Hello是主入口程序为Hello这个类, 第三个和第四个是jar包中需要包含的文件
    • 第二种解决办法: 修改MANIFEST.mf
      • 创建一个MANIFEST-ADD.mf文件, 添加Main-Class: Hello
      • 这里一定需要换行, 保存文件后, 运行jar -ufm Hello.jar MANIFEST-ADD.mf即可

多版本jar

JDK 9引入了多版本jar, 将特定于版本的类文件放在了META-INF/versions

  • 如果要增加不同版本(比如JDK 9)的类文件, 可以使用jar -uf xxx.jar --release 9 xxx.class
  • 如果要从头构建一个多版本jar, 可以使用-C, 每个对应的版本切换到一个不同的类文件目录
    • jar cf xxx.jar -C bin/8 . --release 9 -C bin/9 xxx.class
  • 不同版本的编译, 需要使用--release-d指定输出目录
  • 多版本jar唯一的作用是让你的程序可以使用不同版本的jdk

注释

类注释

  • 放在 import 之后, class 之前
  • 使用\** *\

方法注释

  • 可以对方法的作用, 方法的参数, 返回值, 异常添加注释, 使用@param, @return, @throws

字段注释

  • 只需要对公共字段, 静态常量进行注释

包注释

  • 需要单独写一个文件, 比如package-info.java, 里面是文档注释
  • 或者可以写一个package.html, 里面抽取标记<body>...</body>的所有文本