简介

  • class

  • 字段 field

  • 实例 instance

    • 创建实例

      1
      Person ming = new Person();

      Person ming是定义Person类型的变量ming,而new Person()是创建Person实例。

  • 一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中

方法

直接把fieldpublic暴露给外部可能会破坏封装性。

为了避免外部代码直接去访问field,我们可以用private修饰field,拒绝外部访问。

然后使用方法(method)来让外部代码可以间接修改field外部代码可以调用方法setName()setAge()来间接修改private字段。在方法内部,我们就有机会检查参数对不对。比如,setAge()就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age设置成不合理的值。

setName()方法同样可以做检查,例如,不允许传入null和空字符串:

1
2
3
4
5
6
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("invalid name");
}
this.name = name.strip(); // 去掉首尾空格
}

同样,外部代码不能直接读取private字段,但可以通过getName()getAge()间接获取private字段的值。

1
2
3
public int getAge() {
return calcAge(2019); // 调用private方法
}

this变量

如果没有命名冲突,可以省略this

可变参数

可变参数用类型...定义,可变参数相当于数组类型:

1
2
3
4
5
6
7
class Group {
private String[] names;

public void setNames(String... names) {
this.names = names;
}
}

为什么不用数组Sting[]类型:

  • 调用方需要自己先构造String[],比较麻烦。而可变参数不用
  • 可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null

参数绑定

  • 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
  • 引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
Person p = new Person();
String bob = "Bob";
p.setName(bob); // 传入bob变量
System.out.println(p.getName()); // "Bob"
bob = "Alice"; // bob改名为Alice
System.out.println(p.getName()); // 依旧是"Bob"
}
}

上述特例中,line7的时候给String类型赋值时,不是覆盖,会在内存中新开辟一个空间,bob的指向该变了。但是p.name还是指向原来的内存空间 还是Bob

构造方法

通过构造方法来初始化实例的。我们先来定义一个构造方法,能在创建Person实例的时候,一次性传入nameage,完成初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);
System.out.println(p.getName());
System.out.println(p.getAge());
}
}

class Person {
private String name;
private int age;

public Person(String name, int age) { //构造方法
this.name = name;
this.age = age;
}

public String getName() {
return this.name;
}

public int getAge() {
return this.age;
}
}

构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。

既对字段进行初始化,又在构造方法中对字段进行初始化:

1
2
3
4
5
6
7
8
9
class Person {
private String name = "Unamed";
private int age = 10;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

当我们创建对象的时候,new Person("Xiao Ming", 12)得到的对象实例,字段的初始值是啥?

1
2
3
4
5
6
7
8
9
class Person {
private String name = "Unamed";
private int age = 10;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

在Java中,创建对象实例的时候,按照如下顺序进行初始化:

  1. 先初始化字段,例如,int age = 10;表示字段初始化为10double salary;表示字段默认初始化为0String name;表示引用类型字段默认初始化为null
  2. 执行构造方法的代码进行初始化。

因此,构造方法的代码由于后运行,所以,new Person("Xiao Ming", 12)的字段值最终由构造方法的代码确定。

多构造方法

可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public Person(String name) {
this.name = name;
this.age = 12;
}

public Person() {
}
}

一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}

public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}

方法重载

这种方法名相同,但各自的参数不同,称为方法重载(Overload)。

注意:方法重载的返回值类型通常都是相同的。

继承

继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让StudentPerson继承时,Student就获得了Person的所有功能,我们只需要为Student编写新增的功能。

Java使用extends关键字来实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
private String name;
private int age;

public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}

class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;

public int getScore() { … }
public void setScore(int score) { … }
}

可见,通过继承,Student只需要编写额外的功能,不再需要重复代码。

子类自动获得了父类的所有字段,严禁定义与父类重名的字段!

在OOP的术语中,我们把Person称为超类(super class),父类(parent class),基类(base class),把Student称为子类(subclass),扩展类(extended class)。

protected

子类无法访问父类的private字段或者private方法。

protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问

super

super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName

实际上,有的时候使用super.name,或者this.name,或者name,效果都是一样的。编译器会自动定位到父类的name字段。

但是,在某些时候,就必须使用super

在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();

如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。

解决方法是调用Person类存在的某个构造方法。例如:

1
2
3
4
5
6
7
8
class Student extends Person {
protected int score;

public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}

这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

阻止继承

正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。

从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。

例如,定义一个Shape类:

1
2
3
public sealed class Shape permits Rect, Circle, Triangle {
...
}

上述Shape类就是一个sealed类,它只允许指定的3个类继承它。如果写:

1
public final class Rect extends Shape {...}

是没问题的,因为Rect出现在Shapepermits列表中。

这种sealed类主要用于一些框架,防止继承被滥用。

sealed类在Java 15中目前是预览状态,要启用它,必须使用参数--enable-preview--source 15

向上转型与向下转型

向上转型upcasting

Student是从Person继承下来的,即继承树是Student > Person > Object,则可以

1
Person p = new Student(); 

向下转型downcasting

把一个父类类型强制转型为子类类型

利用instanceof,在向下转型前可以先判断:

1
2
3
4
5
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}

从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。

多态

覆写override

如果方法签名相同,并且返回值也相同,就是Override

加上@Override可以让编译器帮助检查是否进行了正确的覆写。

  • 在必要的情况下,我们可以覆写Object的个方法。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Person {
    ...
    // 显示更有意义的字符串:
    @Override
    public String toString() {
    return "Person:name=" + name;
    }

    // 比较是否相等:
    @Override
    public boolean equals(Object o) {
    // 当且仅当o为Person类型:
    if (o instanceof Person) {
    Person p = (Person) o;
    // 并且name字段相同时,返回true:
    return this.name.equals(p.name);
    }
    return false;
    }

    // 计算hash:
    @Override
    public int hashCode() {
    return this.name.hashCode();
    }
    }
  • 如果要调用父类的被覆写的方法,可以通过super来调用。

多态Polymorphic

Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。

这个非常重要的特性在面向对象编程中称之为多态,即针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。

final

  • final修饰的方法可以阻止被覆写;

    final修饰的class可以阻止被继承;

    final修饰的field必须在创建对象时初始化,随后不可修改。

面向抽象编程:抽象类

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:

1
2
3
class Person {
public abstract void run();
}

把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。

这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。

面向抽象编程的本质就是:

  • 上层代码只定义规范(例如:abstract class Person);
  • 不需要子类就可以实现业务逻辑(正常编译);
  • 具体的业务逻辑由不同的子类实现,调用者并不关心。

接口

如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:interface

1
2
3
4
interface Person {
void run(); //不用加public?
String getName();
}
  • 当一个具体的class去实现一个interface时,需要使用implements关键字。

  • 在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface,例如:

    1
    2
    3
    class Student implements Person, Hello { // 实现了两个interface
    ...
    }
  • 一个interface可以继承自另一个interfaceinterface继承自interface使用extends

相关术语

注意区分术语:

Java的接口特指interface的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。

抽象类和接口的对比如下:

abstract class interface
继承 只能extends一个class 可以implements多个interface
字段 可以定义实例字段 不能定义实例字段
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义default方法

default方法

在接口中,可以定义default方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
public static void main(String[] args) {
Person p = new Student("Xiao Ming");
p.run();
}
}

interface Person {
String getName();
default void run() { //default方法
System.out.println(getName() + " run");
}
}

class Student implements Person {//实现类可以不必覆写default方法。
private String name;

public Student(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

静态字段和静态方法

静态字段

static修饰的字段,称为静态字段:static field

实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。

==> 无论修改哪个实例的静态字段,所有实例的静态字段都被修改了,原因是静态字段并不属于实例

因此,不推荐用实例变量.静态字段去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象。

推荐用类名来访问静态字段。可以把静态字段理解为描述class本身的字段

静态方法

调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {//静态方法
Person.setNumber(99);
System.out.println(Person.number);
}
}

class Person {
public static int number;

public static void setNumber(int value) { //静态方法
number = value;
}
}

因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。

通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。

通常情况下,通过实例变量访问静态字段和静态方法,会得到一个编译警告。

静态方法经常用于工具类。例如:

  • Arrays.sort()
  • Math.random()

静态方法也经常用于辅助方法。注意到Java程序的入口main()也是静态方法。

接口的静态字段

因为interface是一个纯抽象类,所以它不能定义实例字段。

但是,interface是可以有静态字段的,并且静态字段必须为final类型(编译器会自动把该字段变为public static final类型)

注意要把包名写全,不能因为上一层目录一样就不写

在定义class的时候,我们需要在第一行声明这个class属于哪个包。

1
package package_name

包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。

  • 所有Java文件对应的目录层次要和包的层次一致。
  • 位于同一个包的类,可以访问包作用域的字段和方法。不用publicprotectedprivate修饰的字段和方法就是包作用域。
  • 可以通过import导入类名,写的时候就可以忽略包名,直接写类名。import package_name.*表示导入所有类
    • 有一种import static的语法,它可以导入可以导入一个类的静态字段和静态方法很少用

Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:

  • 如果是完整类名,就直接根据完整类名查找这个class
  • 如果是简单类名,按下面的顺序依次查找:
    • 查找当前package是否存在这个class
    • 查找import的包是否包含这个class
    • 查找java.lang包是否包含这个class

如果按照上面的规则还无法确定类名,则编译报错。

因此,编写class的时候,编译器会自动帮我们做两个import动作:

  • 默认自动import当前package的其他class
  • 默认自动import java.lang.*注意:自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入。

如果有两个class名称相同,例如,mr.jun.Arraysjava.util.Arrays,那么只能import其中一个,另一个必须写完整类名。

最佳实践-倒置域名

为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:

  • org.apache
  • org.apache.commons.log
  • com.liaoxuefeng.sample

子包就可以根据功能自行命名。

要注意不要和java.lang包的类重名,即自己的类不要使用这些名字:

  • String
  • System
  • Runtime

要注意也不要和JDK常用类重名:

  • java.util.List
  • java.text.Format
  • java.math.BigInteger

修饰符与作用域

  • Java内建的访问权限包括publicprotectedprivatepackage权限;
  • final修饰符不是访问权限,它可以修饰classfieldmethod
  • 一个.java文件只能包含一个public类(文件名必须和public类的名字相同),但可以包含多个非public类。

public

  • 定义为publicclassinterface可以被其他任何类访问
  • 定义为publicfieldmethod可以被其他类访问,前提是首先有访问class的权限

private

  • 定义为privatefieldmethod无法被其他类访问
    • private访问权限被限定在class的内部,而且与方法声明顺序无关。推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法
  • 如果一个类内部还定义了嵌套类(nested class),那么,嵌套类拥有访问private的权限

protected

  • protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类

package(缺省)

最后,包作用域是指一个类允许访问同一个package的没有publicprivate修饰的class,以及没有publicprotectedprivate修饰的字段和方法。

final

Java还提供了一个final修饰符。final与访问权限不冲突,它有很多作用。

  • final修饰class可以阻止被继承
  • final修饰method可以阻止被子类覆写
  • final修饰field可以阻止被重新赋值
  • final修饰局部变量可以阻止被重新赋值

最佳实践

  • 如果不确定是否需要public,就不声明为public,即尽可能少地暴露对外的字段和方法。

  • 把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。

内部类

Java的内部类可分为Inner Class、Anonymous Class和Static Nested Class三种:

  • Inner Class和Anonymous Class本质上是相同的,都必须依附于Outer Class的实例,即隐含地持有Outer.this实例,并拥有Outer Class的private访问权限;
  • Static Nested Class是独立类,但拥有Outer Class的private访问权限。

要实例化一个Inner,我们必须首先创建一个Outer的实例,然后,调用Outer实例的new来创建Inner实例:

1
2
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

匿名类Anonymous Class

在类内部定义,但不需要明确定义,直接写就行了。定义匿名类的时候就必须实例化它,定义匿名类的写法如下:

1
2
3
Runnable r = new Runnable() {
// 实现必要的抽象方法...
};

除了接口外,匿名类也完全可以继承自普通类。

静态内部类Static Nested Class

static修饰的内部类和Inner Class有很大的不同,它不再依附于Outer的实例,而是一个完全独立的类,因此无法引用Outer.this,但它可以访问Outerprivate静态字段和静态方法。

classpath和jar

classpath

  • classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class

    • Java是编译型语言,源码文件是.java,而编译后的.class文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个abc.xyz.Hello的类,应该去哪搜索对应的Hello.class文件。
    • 所以,classpath就是一组目录的集合,它设置的搜索路径与操作系统相关。例如,在Windows系统上,用;分隔,带空格的目录用""括起来
  • classpath的设定方法有两种:

    • 在系统环境变量中设置classpath环境变量,不推荐;
    • 在启动JVM时设置classpath变量,推荐。
    • 在IDE中运行Java程序,IDE自动传入的-cp参数是当前工程的bin目录和引入的jar包(换句话说一般不用管)

    我们强烈不推荐在系统环境变量中设置classpath,那样会污染整个系统环境。在启动JVM时设置classpath才是推荐的做法。实际上就是给java命令传入-classpath-cp参数:

    1
    java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello

    或者使用-cp的简写;

    没有设置系统环境变量,也没有传入-cp参数,那么JVM默认的classpath.,即当前目录;

  • 不要把任何Java核心库添加到classpath中!JVM根本不依赖classpath加载核心库

jar包

package组织的目录层级,以及各个目录下的所有文件(包括.class文件和其他文件)都打成一个jar文件

实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class,就可以把jar包放到classpath中:

1
java -cp ./hello.jar abc.xyz.Hello

这样JVM会自动在hello.jar文件里去搜索某个类。

创建jar包

找到相应文件夹压缩成zip,改后缀为jar就可以了

jar包里的第一层目录,不能是bin,而应该是hongmingmr等包

maven

jar包还可以包含一个特殊的/META-INF/MANIFEST.MF文件,MANIFEST.MF是纯文本,可以指定Main-Class和其它信息。

JVM会自动读取这个MANIFEST.MF文件,如果存在Main-Class,我们就不必在命令行指定启动的类名,而是用更方便的命令:

1
java -jar hello.jar

jar包还可以包含其它jar包,这个时候,就需要在MANIFEST.MF文件里配置classpath了。

在大型项目中,不可能手动编写MANIFEST.MF文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如==Maven==,可以非常方便地创建jar包。

模块

从Java 9开始引入的模块,主要是为了解决“依赖”这个问题

了表明Java模块化的决心,从Java 9开始,原有的Java标准库已经由一个单一巨大的rt.jar分拆成了几十个模块,这些模块以.jmod扩展名标识,可以在$JAVA_HOME/jmods目录下找到它们

  • java.base.jmod
  • java.compiler.jmod
  • java.datatransfer.jmod
  • java.desktop.jmod

模块之间的依赖关系已经被写入到模块内的module-info.class文件了。

所有的模块都直接或间接地依赖java.base模块,只有java.base模块不依赖任何模块,它可以被看作是“根模块”

编写模块等

模块 - 廖雪峰的官方网站 (liaoxuefeng.com)

bin目录存放编译后的class文件,src目录存放源码,按包名的目录结构存放,仅仅在src目录下多了一个module-info.java这个文件,这就是模块的描述文件。在这个模块中,它长这样:

1
2
3
4
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入java.base
requires java.xml;
}

其实就是把在源码中 用到的模块/包都写在这里,及依赖关系

编译后存放到bin目录下

1
$ javac -d bin src/module-info.java src/com/itranswarp/sample/*.java

把bin目录下的所有class文件先打包成jar,在打包的时候,注意传入--main-class参数,让这个jar包能自己定位main方法所在的类

1
$ jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin .

以直接使用命令java -jar hello.jar来运行它,创建模块的话继续使用JDK自带的jmod命令把一个jar包转换成模块:

1
$ jmod create --class-path hello.jar hello.jmod

打包JRE

裁剪JRE:jilink

1
$ jlink --module-path [指定模块].jmod --add-modules [java.base,用到的模块...] --output jre/

--module-path参数指定了我们自己的模块hello.jmod,然后,在--add-modules参数中指定了我们用到的3个模块java.basejava.xmlhello.world,用,分隔。最后,在--output参数指定输出目录。

在当前目录下,我们可以找到jre目录,这是一个完整的并且带有我们自己hello.jmod模块的JRE。试试直接运行这个JRE:

1
2
$ jre/bin/java --module hello.world
Hello, xml!

打包分发,对方不需要下载安装JDK,直接执行以上命令就可以

访问权限

只有它声明的导出的包,外部代码才被允许访问。换句话说,如果外部代码想要访问我们的hello.world模块中的com.itranswarp.sample.Greeting类,我们必须将其导出:

1
2
3
4
5
6
module hello.world {
exports com.itranswarp.sample;

requires java.base;
requires java.xml;
}

因此,模块进一步隔离了代码的访问权限。

总结

  • 打包模块:编写module-info,java;编译放入bin/底下,.java.class;打包bin目录下class,转为jmod模块
  • 打包jre:jilink命令

IDEA打包编译方式

文件-项目结构,选择相应模块-路径,选择输出路径

构建——构建模块(等一段时间)

IntelliJ IDEA如何手动编译项目-百度经验 (baidu.com)

然后【工具】IDEA打包jar包_shankezh的博客-CSDN博客_idea打包工具

构建-构建工件