Java–面向对象基础
Java是一种面向对象的编程语言。面向对象编程,英文是Object-Oriented Programming
,简称OOP
。
和面向对象编程不同的,是面向过程编程。面向过程编程,是把模型分解成一步一步的过程。
一、基础
面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。
现实世界中,我们定义了“人” 这种抽象概念,而具体的人则是“小明”,“小红”等一个个具体的人。所以“人”可以定义为一个类(class
),而具体的人则是实例(instance
)
现实世界 | 计算机模型 | Java代码 |
---|---|---|
人 | 类 / class | class Person { } |
小明 | 实例 / ming | Person ming = new Person() |
小红 | 实例 / hong | Person hong = new Person() |
小军 | 实例 / jun | Person jun = new Person() |
1.1、class和instance
所以,只要理解了class
和instance
的概念,基本上就明白了面向对象编程。
class
是一种对象模板,它定义了如何创建实例,因此class
本身就是一种数据类型
instance
是对象实例,instance
是根据class
创建的实例,可以创建多个instance
,每个instance
类型相同,但是各自属性可能不同
1.2、定义class
在Java中创建一个类class
class Person {
public String name;
public int age;
}
一个class
可以包含多个字段(field
),字段用来描述一个类的特征。上面的Person
类,定义了两个字段,一个是String
类型的字段,命名为name
,一个是int
类型的字段,命名为age
。因此,通过class
,把一组数据汇集到一个对象上,实现了数据封装。
public
是用来修饰字段的,他表示这个字段可以被外部访问。
1.3、创建实例
定义了class
,只是定义了对象模板,要根据模板创建真正的对象实例,必须使用new
操作符
new
操作符可以创建一个实例,然后需要定义一个引用类型的变量来指向这个实例:
Person ming = new Person();
//创建Person实例,并通过变量 ming 指向它
Person ming
是定义Person
类型的变量ming
,而new Person()
是创建Person
实例
//有了指向实例的变量后,可通过 变量.字段 对实例进行访问
ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
System.out.println(ming.name); // 访问字段name
Person hong = new Person();
hong.name = "Xiao Hong";
hong.age = 15;
一个Java源文件可以包含多个类的定义
,但只能定义一个public类
,且public类名必须与文件名一致
。如果要定义多个public类,必须拆到多个Java源文件中。
二、方法
一个class
可以包含多个field
,如下,给Person
类定义两个field
class Person {
public String name;
public int age;
}
但是直接把field
用public
暴露给外部,可能会破坏封装性,如下:
Person ming = new Person();
ming.name = "小明";
ming.age = -99; // 年龄设置为负数
显然,直接操作field
,容易造成逻辑混乱.为了避免外部代码直接去访问field
,可以使用private
修饰field
,拒绝外部访问:
public class Main{
public static void main(String[] args){
Person ming = new Person();
ming.name = "明明";
ming.age = 3; //外部代码不能访问 private 修饰的 field,编译报错
}
}
class Person {
private String name;
private int age;
}
外部代码不能访问
private
修饰的field
, 需使用方法(method
)来让外部代码间接修改field
public class OopStudy {
public static void main(String[] args){
Animal cat = new Animal();
cat.setName("咪咪");
cat.setAge(1);
System.out.println("姓名:"+cat.getName());
System.out.println("年龄:"+cat.getAge());
}
}
class Animal {
private String name;
private int age;
//设置字段
public void setName(String name) {
if(name == null || name.isBlank()){
throw new IllegalArgumentException("名字不能为空")
}
this.name = name.strip();//去除首尾空格
}
public void setAge(int age) {
if(age < 0 ) {
throw new IllegalArgumentException("年龄参数不正确");
}
this.age = age;
}
//获取字段
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
}
虽然外部代码不能直接修改private
修饰的字段,但可以通过public
修饰的方法进行间接修改,在方法中就可以对传入的参数进行判断是否合理.
2.1、定义方法
定义方法的语法:
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
方法返回值通过return
语句实现,如果没有返回值,返回类型设置为void
,可以省略return
需注意,和private
修饰的字段一样, private
方法不允许外部直接调用,只能通过public
方法进行间接调用
2.2、this变量
在方法内部,可以使用一个隐含的变量this
,他始终指向当前实例
. 因此,通过this.field
就可以访问当前实例的字段
如果没有命名冲突,可以省略this
class Person {
private String name;
public String getName() {
return name; //相当于this.name
}
}
但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
class Person {
private String name;
public void setName(String name) {
this.name = name; //若缺少this,将无法进行成功赋值
}
}
2.3、方法参数
方法的参数可为任意个数, 但是调用方法时,必须严格按照参数的定义一一传递
class Person {
...
public void setNameAndAge(String name, int age) {
...
}
}
调用setNameAndAge()
方法时,必须有两个参数,且第一个参数必须为String
,第二个参数必须为int
Person ming = new Person();
ming.setNameAndAge("Xiao Ming"); // 编译错误:参数个数不对
ming.setNameAndAge(12, "Xiao Ming"); // 编译错误:参数类型不对
2.4、可变参数
可变参数用参数...
定义,可变参数相当于数组类型:
class Group {
private String[] names;
public void setName(String... names) {
this.names = names;
}
}
这样调用setName()
时,可传入任意个参数
Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String
可变参数和String[]
类型参数的对比
class Group {
private String[] names;
public void setNames(String[] names) {
this.names = names;
}
}
String[]
参数在调用方法时,需自己先构造String[]
,比较麻烦
Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
另一个问题是,调用方法可以传入null
Group g = new Group();
g.setNames(null);
而可变参数可以保证无法传入null
,因为传入0个参数时,接受到的实际值是一个空数组而不是null
2.5、参数绑定
调用方把参数传递给实例方法时,调用时传递的值会按照参数位置(可理解为:内存地址
)一一绑定
//基本类型参数绑定
public class Main {
public static void main(String[] args) {
Person p = new Person();
int n = 15; // n的值为15
p.setAge(n); // 传入n的值
System.out.println(p.getAge()); // 15
n = 20; // n的值改为20
System.out.println(p.getAge()); // 15还是20?
}
}
class Person {
private int age;
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
//结果
15
15
运行代码,从结果可知,修改外部的局部变量n
,不影响实例p
的age
字段,原因是setAge()
方法获得的参数,复制了n
的值,因此,p.age
和局部变量n
互不影响。
结论:基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
//引用类型参数绑定
public class Main {
public static void main(String[] args) {
Person p = new Person();
String[] fullname = new String[] { "Homer", "Simpson" };
p.setName(fullname); // 传入fullname数组
System.out.println(p.getName()); // "Homer Simpson"
fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
System.out.println(p.getName()); // "Homer Simpson"还是"Bart Simpson"?
}
}
class Person {
private String[] name;
public String getName() {
return this.name[0] + " " + this.name[1];
}
public void setName(String[] name) {
this.name = name;
}
}
//结果
Homer Simpson
Bart Simpson
注意到setName()
的参数现在是一个数组。一开始,把fullname
数组传进去,然后,修改fullname
数组的内容,结果发现,实例p
的字段p.name
也被修改了!
应用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为都指向同一个内存地址)
注意
// 引用类型 String 的参数绑定
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"还是"Alice"?
}
}
class Person {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
//结果
Bob
Bob
为什么没有成为“Alice”?
因为字符串具有不可变特性
,在执行bob = "Alice"
时,会创建 “Alice” ,并将 bob 指向它。
而原本的 “Bob” 还存在没有被修改或删除,实例的name的指向也一直时Bob
三、构造方法
可类比为
JS
的ES6中class类的构造函数constructor
在上面的案例中,实例化对象后,需调用setName()
和setAge()
两个方法,否则内部状态是不正常的
Person ming = new Person();
ming.setName("小明");
ming.setAge(12);
若想在创建对象实例时,就把内部的字段全部初始化为合适的值,就需要构造方法
public class Main{
public static void main(String[] args) {
Person xm = new Person("小明",15);
System.out.println(xm.getName());
System.out.println(xm.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
操作符
3.1、默认构造方法
任何class
都有构造方法
为什么之前没有定义Person
类编写构造方法,还能调用new Person()
原因是如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,他没有参数,也没有执行语句,类似这样:
class Person {
public Person() {
}
}
注意:如果自定义了一个构造方法,那么编译器就
不再
自动创建默认构造方法
public class Main {
public static void main(String[] args){
Person xm = new Person(); //编译报错:找不到这个方法
}
}
class Person {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
}
若想保留两种构造方法,那么就只能将两中方法都写出来
public class Main {
public static void main(String[] args){
Person xm = new Person(); //可以调用无参数构造方法
Person xm = new Person("小米",6); //也可以调用带参数的构造方法
}
}
class Person {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public Person(){
}
}
没有在构造方法中初始化字段时,引用类型的字段默认是null
,数值类型的字段用默认值,int
类型为0
,布尔型为false
class Person {
private String name; // 默认初始化为null
private int age; // 默认初始化为0
//也可以对字段直接初始化
/*
private String name = "Unamed";
private int age = 10;
*/
public Person() {
}
}
因为在java中创建对象的时候,先初始化字段,再执行构造方法的代码进行初始化。
所以在既对字段初始化,又在构造方法中对字段进行初始化后,
字段的值最终由构造方法的代码确定
3.2、多构造方法
可以定义多个构造方法,在通过new
操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:
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() {
}
}
如果调用new Person("Xiao Ming", 20);
,会自动匹配到构造方法public Person(String, int)
。
如果调用new Person("Xiao Ming");
,会自动匹配到构造方法public Person(String)
。
如果调用new Person();
,会自动匹配到构造方法public Person()
。
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(...)
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)
}
}
四、方法重载
在一个类中,可以定义多个方法,如果有一系列方法,他们的功能是类似的,只有参数不同,那么可以把这一组方法名做成同名方法
。如下,定义了多个hello()
方法:
class Hello {
public void hello() {
System.out.println("Hello, world!");
}
public void hello(String name) {
System.out.println("Hello, " + name + "!");
}
public void hello(String name, int age) {
if (age < 18) {
System.out.println("Hi, " + name + "!");
} else {
System.out.println("Hello, " + name + "!");
}
}
}
这种方法名相同,但各自参数不同的,被称为方法重载(Overload
)
注意:方法重载的返回值类型通常都是相同的
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此调用更简单
举个例子,String
类提供了多个重载方法indexOf()
,可以查找子串:
int indexOf(int ch)
:根据字符的Unicode码查找;int indexOf(String str)
:根据字符串查找;int indexOf(int ch, int fromIndex)
:根据字符查找,但指定起始位置;int indexOf(String str, int fromIndex)
根据字符串查找,但指定起始位置。
五、继承
现在有两个类
// Person
class Person {
private String name;
private int age;
public String getName(){...}
public int getAge(){...}
public void setName(){...}
public void setAge(){...}
}
// Student
class Student {
private String name;
private int age;
private int score;
public String getName(){...}
public int getAge(){...}
public int getscore(){...}
public void setName(){...}
public void setAge(){...}
public void setscore(){...}
}
Student
类包含了Person
类已有的字段和方法,且有新的字段和方法
为了减少重复的代码,可以使用继承
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码,当我们让Student
继承Person
时。Student
就获得了Person
的所有功能,只需为Student
编写新增的功能
5.1、extends关键字
Java中使用extends
关键字来实现继承
class Person {
private String name;
private int age;
public String getName(){...}
public int getAge(){...}
public void setName(){...}
public void setAge(){...}
}
class Student extends Person{
//不要重复name和age字段/方法
//只需要定义新增的字段/方法
private int score;
public int getScore(){...}
public void setScore(){...}
}
注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!
在OOP的术语中:
-
Person
称为超类(super class)、父类(parent class)、基类(base class) -
Student
称为子类(sub class)、扩展类(extended class)
5.2、继承树
可以理解为 JS 中的原型链???
我们在定义Person
的时候,没有写extends
,是因为,在Java中,没有明确写extends
的类,编译器会自动加上extends Object
。所有,任何类,除了Object
,都会继承某个类。
下方是Person
和Student
的继承树:
┌───────────┐
│ Object │
└───────────┘
▲
│
┌───────────┐
│ Person │
└───────────┘
▲
│
┌───────────┐
│ Student │
└───────────┘
Java只允许一个class继承一个类,因此,一个类有且仅有一个父类。只有Object
是特殊的,他没有父类。
若再新增一个继承Person
的Teacher
类。继承树关系如下:
┌───────────┐
│ Object │
└───────────┘
▲
│
┌───────────┐
│ Person │
└───────────┘
▲ ▲
│ │
│ │
┌───────────┐ ┌───────────┐
│ Student │ │ Teacher │
└───────────┘ └───────────┘
5.3、protected
使用继承后,子类无法访问父类的private
字段或private
方法.
若想访问父类字段,需将private
改为protected
,方法同理。
class Person {
private String name = "张三";
protected int age = 24;
}
class Student extends Person {
public String hello(){
return "Hello" + name; //直接使用name 或 使用this.name编译器会自动定位到父类的 name 字段
return "Hello" + this.name; //编译错误,无法访问name字段
}
public int showAge(){
return "Age: " + this.name; //ok
}
}
protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类以及子类所访问。
5.4、super
super
关键字表示父类(超类),子类引用父类的字段是,可以用super fieldName
class Student extends Person {
public String hi() {
return "hi" + super.name;
}
}
这里使用super.name
,this.name
,name
的效果都是一样的。编译器会自动定位到父类的name
字段。
某些时候必须使用super
,如下:
public class Main {
public static void main(String[] args) {
Student s = new Student("Xiao Ming", 12, 89);
}
}
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
this.score = score;
}
}
上方代码,会编译报错。
因为Java中,任何class
的构造方法,第一行必须是调用父类的构造方法,如果没有明确的调用父类的构造方法,编译器会自动加一句super();
,所以,Student
类的构造方法实际上是这样的:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(); // 自动调用父类的构造方法 (理解为调用了父类的 public Person(){...}?
this.score = score;
}
}
但是,Person
类并没有无参数的构造方法,因此编译失败
解决办法是调用Person
类存在的某个构造方法。
class Student extends Person {
private int score;
public Student (String name, int age ,int score) {
super(name,age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}
如果父类没有和子类构造方法
参数
相对应的构造方法,子类就必须显式调用super()
,并给出参数以便让编译器定位到父类的一个合适的构造方法
由此可见:
子类 不会继承 任何父类的构造方法
,子类默认的构造方法是编译器自动生成的,不是继承的
5.5、阻止继承
正常情况下,只要某个class没有final
修饰符,那么任何类后可以从该class继承。
从Java 15 开始,允许使用sealed
修饰class,并通过permits
明确能够从该class继承的子类名称
public sealed class Human permits Man, Woman{
...
}
Human
类为一个sealed
类,只允许Man
和Woman
继承它
public final class Man extends Human {...} // 使用final修饰后,Man类无法再被继承
public final class Kid extends Human {...} // Kid没有被允许继承,会报错
这种
sealed
类主要用于一些框架,防止继承被滥用。
sealed
类在Java 15中目前是预览状态,要启用它,必须使用参数--enable-preview
和--source 15
。
5.6、向上转型
如果一个引用变量的类型是Student
,那么它可以指向一个Student
类型的实例
Student s = new Student();
// 同理
Person p = new Person();
如果Student
是从Person
继承下来的,那么引用类型为Person
的变量,能够指向Student
类型的实例
Person p = new Student();
这是因为Student
继承自Person
,因此它拥有Person
的全部功能。Person
类型的变量指向Student
类型的实例,对它进行操作是没有问题的
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
向上转型实际上是把一个子类型安全的变为更加抽象的父类型
Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok
注意到继承树是Student > Person > Object
,所以,可以把Student
类型转型为Person
,或者更高层次的Object
。
5.7、向下转型
和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
Person
类型p1
实际指向Student
实例,
Person
类型变量p2
实际指向Person
实例。
在向下转型的时候,把p1
转型为Student
会成功,因为p1
确实指向Student
实例,
把p2
转型为Student
会失败,因为p2
的实际类型是Person
,
不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。
为了避免向下转型出错,Java提供了instanceof
操作符,可以先判断一个实例究竟是不是某种类型:
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false
Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true
Student n = null;
System.out.println(n instanceof Student); // false
instanceof
实际上判断一个变量指向的实例是否是指定类型,或者这个类型的子类。
如果一个引用变量为null
,那么对任何instanceof
的判断都为false
。
Person p = new Student();
if(p instanceof Student) {
//判断通过才转换
Student s = (Student) p;
}
从Java 14开始,判断instanceof
后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
//可改写为
Object obj = "hello";
if (obj instanceof String s) {
// 可以直接使用变量s:
System.out.println(s.toUpperCase());
}
5.8、区分继承和组合
如果现在有一个Book
类,拥有name
字段,能否让Student
继承自Book
?
从逻辑上,这样是不合理的,因为Student
是Person
的一种,是is关系,而Student
不是Book
,它们应该为has关系。
has关系不能使用继承,而应使用组合,即Student
可以持有一个Book
实例;
class Student extends Person{
protected Book book;
protected int score;
}
继承是
is
关系,组合是has
关系
六、多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同(方法名相同,方法参数相同,方法返回值相同
)的方法,被称为覆写(Override)
//父类中有 run() 方法
class Person {
public void run() {
System.out.println("Person.run");
}
}
//子类中也有 run() 方法
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
Override和Overload不同的是,如果方法签名不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override
。
注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
class Person {
public void run() { … }
}
class Student extends Person {
// 不是Override,因为参数不同:
public void run(String s) { … }
// 不是Override,因为返回值不同:
public int run() { … }
}
6.1、@Override
如果想让编译器帮助检查是否进行了正确的覆写(即检查是否正确属性方法签名),需在方法上方加上@Override
,若签名书写错误将报错。
public class Main {
public static void main(String[] args) {
}
}
class Person {
public void run() {}
}
public class Student extends Person {
@Override // Compile error!
public void run(String s) {}
}
但@Override
不是必须的,因为引用变量的声明类型可能和实际类型不符,例如:
Person p = new Student();
自我理解:
声明类型—>为这个变量能拥有功能的最大范围,
实际类型—>为这个变量实际拥有功能的范围
public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run(); // 应该打印Student.run
}
}
class Person {
public void run() {
System.out.println("Person.run");
}
}
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
一个实际类型为Student
,引用类型为Person
的变量,调用其run()
方法,调用的是Student
的run()
方法
因此可得出结论:
Java的实例方法调用是基于运行时的
实际类型
的动态调用,而非变量的声明类型
这个非常重要的特性在面向对象编程中称之为多态
。英文:Polymorphic
。
6.2、多态
多态是指,针对某个类型的方法调用,其
真正执行的方法取决于
运行时期实例类型的方法
。
public void runTwice(Person p) {
p.run();
p.run();
}
上发例子中,传入的参数类型是Person
,但是无法知道传入的参数实际类型究竟是Person
还是Student
,或者是Person
的其他子类,因此也无法确定调用的是不是Person
类定义的run()
方法。
多态的特性就是,运行期才能动态决定调用的子类方法,对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法
多态应用例子:
计算税收:
public class ShowTax {
public static void main(String[] args){
// Income[]表示数组中的元素为Income类型
// new Income[] 表示实例化一个Income类型的数组
Income[] myIncomes = new Income[] {
new Income(3000),
new Salary(7500),
new SpecialIncome(15000)
};
totalTax(myIncomes);
}
// 计算方法
// showTax(Income... incomes) 表示传入的参数为Income数组
public static void totalTax(Income... incomes){
double totalTax = 0;
for (Income item:incomes) {
totalTax += item.getTax();
}
System.out.println("总应缴税:"+ totalTax);
}
}
//====== 计算普通收入 10%
class Income{
protected double income;
//自定义构造方法
public Income(double income){
this.income = income;
}
public double getTax(){
return income * 0.1;
}
}
//继承 income
//===== 工资收入 > 5000 ? 20%
class Salary extends Income{
// 自定义构造方法
// super(income)表示使用父类中签名一致的构造方法 --> 构造方法(income)
public Salary(double income){
super(income);
}
@Override
public double getTax(){
if (income < 5000){
return 0;
}
return (income - 5000) * 0.2;
}
}
//继承 income
//====== 特殊津贴
class SpecialIncome extends Income{
public SpecialIncome(double income){
super(income);
}
@Override
public double getTax(){
return 0;
}
}
利用多态,totalTax()
方法只需要和Income
打交道,他完全不需要知道Salary
和SpecialIncome
的存在,就可以正确的计算出总的税
若现在新增了一笔收入,只需继承Income
,并正确覆写getTax()
,再把新的类型传入totalTax()
即可。不需要修改任何代码。
多态允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
6.3、覆写Object方法
因为所有的class
最终都继承自Object
,而Obeject
定义了几个重要的方法:
instance:实例
toString()
:把instance输出为String
;equals()
:判断两个instance是否逻辑相等;hashCode()
:计算一个instance的哈希值。
必要情况下可以覆写Object
的这几个方法
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();
}
}
6.4、调用super
在子类的覆写方法中,如果要调用父类被覆写的方法,可以通过super
来调用:
class Person {
protected String name;
public String hello() {
return "Hello, " + name;
}
}
Student extends Person {
@Override
public String hello() {
// 调用父类的hello()方法:
return super.hello() + "!";
}
}
6.5、final
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,就使用final
进行修饰
用
final
修饰的方法不能被Override
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}
Student extends Person {
// compile error: 不允许覆写
@Override
public String hello() {
}
}
用
final
修饰的类不能被继承
final class Person {
protected String name;
}
// compile error: 不允许继承自Person
Student extends Person {
}
用
final
修饰的字段在初始化后不能被修改
class Person {
public final String name = "Unamed";
}
Person p = new Person();
//对 final 修饰的字段重新赋值会报错
p.name = "New Name"; // compile error!
可以在构造函数中初始化final
字段
class Person {
public final String name;
public Person(String name) {
this.name = name;
}
}
这种方法更为常用,因为可以保证实例一旦创建,其
final
字段就不可修改。
final总结:
final
修饰符有多种作用:final
修饰的方法可以阻止被覆写;final
修饰的class可以阻止被继承;final
修饰的field必须在创建对象时初始化,随后不可修改。
七、抽象类
由于多态的存在,每个子类都可以覆写父类的方法,
class Person{
public void run(){...}
}
class Student extends Person{
@Override
public void run(){...}
}
class Teacher extends Person {
@Override
public void run(){...}
}
从Person
类派生的Student
和Teacher
都可以覆写run()
方法。
如果父类Person
的run()
,没有实际意义,能否去掉方法的执行语句{...}
?
class Person {
public void run(); // Compile Error!
}
答案是不行的,会导致编译错误,因为定义方法的时候,必须实现方法的语句
也不能去掉父类的run()
方法,因为去掉之后就失去了多态的特性,下面的例子就无法编译:
public void runTwice(Person p) {
p.run(); // Person没有run()方法,会导致编译错误
p.run();
}
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么可以把父类的方法声明为抽象方法:
class Person{
public abstract void run();
}
把一个方法声明为abstract
,表明它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以Person
类也无法被实例化。编译器会提示:无法编译Person
类,因为它包含抽象方法。
必须把Person
类本身也声明为abstract
,才能正确编译它
abstract class Person {
public abstract void run();
}
7.1、抽象类
如果一个class
定义了方法,但没有具体执行代码,这个方法就是抽象类,抽象方法用abstract
修饰。
因为无法执行抽象方法,因此这个类也必须声明为抽象类(abstract class)
使用abstract
修饰的类就是抽象类。我们无法实例化一个抽象类:
Person p = new Person();//编译错误
抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。
抽象方法实际上相当于定义了"规范"
如下:Person
类定义了抽象方法run()
,那么,在实现子类Student
的时候,就必须覆写run()
方法:
public class Main{
public static void main(String[] args) {
Person P = new Student();
p.run()
}
}
abstract class Person {
public abstract void run();
}
class Student extends Person {
@Override
public void run(){...}
}
6.2、面向抽象编程
当我们定义了抽象类Person
,以及具体的Student
、Teacher
子类的时候,我们可以通过抽象类Person
类型去引用具体的子类的实例:
Person s = new Student;
Person t = new Teacher;
这种引用抽象类的好处在于,我们对其进行方法调用时,并不关心Person
类型变量的具体子类类型:
//不关心新的子类是如何实现run()方法的:
Person e = new Employee();
e.run();
如上:执行
e.run()
时,不关心Person
的子类Employee
中是如何实现run()
方法,我们只需调用即可
这种尽量引用高层类型,避免引用实际子类型的方法,称之为面向抽象编程。
面向抽象编程的本质就是:
- 上层代码只定义规范 (例如:abstract class Person );
- 不需要子类就可以实现逻辑业务 (正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心
八、接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
8.1、接口
如果一个抽象类没有字段,所有方法全部都是抽象方法:
abstract class Person {
public abstract void run();
public abstract String getName();
}
就可以把该抽象类改写为接口:interface
。
在java中,使用interface
可以声明一个接口:
interface Person{
void run();
String getName();
}
所谓interface
,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有
接口定义的所有方法默认都是public abstract
的,所以这两个修饰符不需要写出来(写不写效果都是一样的)
8.2、实现接口
当一个具体的class
去实现一个interface
时,需要使用implement
关键字:
class Student implements Person{
private String name;
public Student(String name){
this.name = name;
}
@Override
public void run(){
System.out.println(this.name + "run");
}
@Override
public String getName(){
return this.name;
}
}
在Java中,一个类只能继承自另一个类,不能从多个类继承。但是一类可以实现多个interface
:
class Student implements Person, Hello { //实现了两个 interface
...
}
8.3、术语
注意区分术语:
Java的接口特指interface
的定义,表示一个接口类型和一组方法签名,
而编程接口泛指接口规范,如方法签名、数据格式、网络协议等。
抽象类和接口的对比如下:
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | 不能定义实例字段 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
8.4、接口继承
一个interface
可以继承子另一个interface
。interface
继承自interface
使用extends
,它相当于扩展了接口的方法。
interface Hello{
void hello();
}
interface Person extends Hello{
void run();
String getName();
}
此时,Person
接口继承自Hello
接口,因此Person
接口实际上有3个抽象方法签名,其中一个来自继承的Hello
接口。
8.5、继承关系
合理设计interface
和abstract class
的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在abstract class
中,具体逻辑放到各个子类,而接口层次表达抽象程度。
参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系:
┌───────────────┐
│ Iterable │
└───────────────┘
▲ ┌───────────────────┐
│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │
▲ ▲ ┌───────────────────┐
│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │
▲ ┌───────────────────┐
└──────────│ AbstractList │
└───────────────────┘
▲ ▲
│ │
│ │
┌────────────┐ ┌────────────┐
│ ArrayList │ │ LinkedList │
└────────────┘ └────────────┘
在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:
List list = new ArrayList(); // 用 List 接口引用具体子类的实例
Collection coll = list; // 向上转型为 Collection 接口
Iterable it = coll; // 向上转型为 Iterable 接口
8.6、default方法
在接口中,可以定义default
方法。例如,把Person
接口的run()
方法改为default
方法:
public class Main {
public static void main(String[] args) {
Person p = new Student("Xiao Ming");
p.run();
}
}
interface Person {
String getName();
default void run() {
System.out.println(getName() + " run");
}
}
class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
实现类可以不必覆写default
方法。default
方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default
方法和抽象类的普通方法是有所不同的。因为interface
没有字段,default
方法无法访问实例字段,而抽象类的普通方法可以访问实例字段。
九、静态字段和静态方法
9.1、静态字段
在一个class
中定义的字段,我们称之为实例字段。实例字段的特点是:每个实力都有独立的字段,每个实例的同名字段互不影响。
还有一种字段,是用static
修饰的字段,称为静态字段:static field
。
实例字段在每个实例中都有自己的一个独立"空间",但是静态字段只有一个共享"空间",所有实例都会共享该字段:
class Person{
public String name;
public int age;
// 定义静态字段number
public static int number;
}
public class Main {
public static void main(String[] args) {
Person ming = new Person("Xiao Ming", 12);
Person hong = new Person("Xiao Hong", 15);
ming.number = 88;
System.out.println(hong.number);
//88
hong.number = 99;
System.out.println(ming.number);
//99
}
}
class Person {
public String name;
public int age;
public static int number;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例
┌──────────────────┐
ming ──>│Person instance │
├──────────────────┤
│name = "Xiao Ming"│
│age = 12 │
│number ───────────┼──┐ ┌─────────────┐
└──────────────────┘ │ │Person class │
│ ├─────────────┤
├───>│number = 99 │
┌──────────────────┐ │ └─────────────┘
hong ──>│Person instance │ │
├──────────────────┤ │
│name = "Xiao Hong"│ │
│age = 15 │ │
│number ───────────┼──┘
└──────────────────┘
虽然实例可以访问静态字段,但是它们指向的其实都是Person class
的静态字段。所以,所有实例共享一个静态字段。
因此,不推荐用实例变量.静态字段
去访问访问静态字段,
因为在Java程序中,实例对象没有静态字段。
在代码中,实例对象能访问访问今天字段只是因为编译器可以更具实例类型自动转化为类名.静态字段
来访问静态字段。
推荐用类名来访问静态字段。可以把静态字段理解为描述class
本身的字段(非实例字段)。
上面代码更好的写法是;
Person.number = 99;
System.out.println(Person.number);
9.2、静态方法
有静态字段,就有静态方法。用static
修饰的方法被称为静态方法。
调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其他编程语言的函数:
public class Main{
public static void main(String[] args) {
Person.setNumber(666);
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()
也是静态方法。
9.3、接口的静态字段
因为interface
是一个纯抽象类,所以他不能定义实例字段。但是,interface
是可以有静态字段的,而且静态字段必须为final
类型:
public interface Person{
public static final int MALE = 1;
public static final int FEMALE = 2;
}
实际上,因为interface
的字段只能是public static final
类型,所以我们可以把修饰符都去掉:
public interface Person{
MALE = 1;
FEMALE = 2;
}
编译器会自动发该字段变为public static final
类型
十、包
在Java中,我们使用package
来解决名字冲突。
Java定义了一种名字空间,称之为包:package
。一个类总是属于某个包,类名(比如Person
只是一个简写,真正完整类名是包名.类名
例如:
小明的Person
类存放在包ming
下面,因此,完整类名是ming.Person
;
小红的Person
类存放在包hong
下面,因此,完整类名是hong.Person
;
小军的Arrays
类存放在包mr.jun
下面,因此,完整类名是mr.jun.Arrays
;
JDK的Arrays
类存放在包java.util
下面,因此,完整类名是java.util.Arrays
。
在定义class
的时候,我们需要在第一行声明这个class
属于哪个包。
小明的Person.java
文件:
package ming; //申明包名ming
public class Person{
}
小军的Arrays.java
文件:
package mr.jun;// 声明包名mr.jun
public class Arrays{
}
在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同。
包可以是多层结构,用.
隔开。例如:java.util
。
要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
没有定义报名的class
,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。
将上方例子按照包结构组织起来,假设package_sample
作为根目录,src
作为源码目录:
package_sample
└─ src
├─ hong
│ └─ Person.java
│ ming
│ └─ Person.java
└─ mr
└─ jun
└─ Arrays.java
所有Java文件对应的目录层次要和包的层次一致。
编译后的.class
文件一需要按照包结构存放,如果使用IDE,将编译后的,class
文件放到bin
目录下:
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
编译的命令相对比较复杂,我们需要在src
目录下执行javac
命令:
javac -d ../bin ming/Person.java hong/Person.java mr/jun/Arrays.java
在IDE中,会自动根据包结构编译所有Java源码,所以不必担心使用命令行编译的复杂命令。
10.1、包作用域
位于同一个包的类,可以访问包作用域的字段和方法,不用public
、protetced
、private
修饰的字段和方法就是作用域,下方Person
类定义在hello
包下面:
package hello;
public class Person {
// 包作用域:
void hello() {
System.out.println("Hello!");
}
}
10.2 、import
在一个class
中,我们总会引用其他的class
。例如,小明的ming.Person
类,如果要引用小军的mr.jun.Arrays
类,他有三种写法:
第一种,直接写出完整类名,例如:
package ming;
public class Person{
public void run(){
mr.jun.Arrays arrys = new mr.jun.Arrays();
}
}
第二种,使用import
语句。导入小军的Arrays
,让后单写名称
// Person.java
package ming;
// 导入完整类名:
import mr.jun.Arrays;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
在写import
的时候,可以使用*
,表示把这个包下面的所有class
都导入进来(但不包括子包的class
):
// 导入mr.jun包的所有class:
import mr.jun.*;
我们一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays
类属于哪个包。
第三种import static
语法,可以导入一个类的静态字段和静态方法。
package main;
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}
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.Arrays
和java.util.Arrays
,那么只能import
其中一个,另一个必须写完整类名。
10.3、包名注意事项
为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:
- 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中,经常看到public
、protected
、private
这些修饰符。这些修饰符可以用来限定访问作用域。
11.1、public
定义为public
的class
、interface
可以被其他任何类访问
package abc;
public class Hello {
public void hi(){
}
}
上面的Hello
是public
,因此可以被其他包访问:
package xxx;
class Main(){
void foo(){
//Main可以访问Hello
Hello h = new Hello();
}
}
定义为public
的field
、method
可以被其他类访问,前提是首先有访问class
的权限:
package abc;
public class Hello {
public void hi(){
}
}
上面的hi()
方法是public
,可以被其他类调用,前提是首先要能访问Hello
类:
package xyz;
class Main{
void foo(){
Hello h = new Hello();
h.hi();
}
}
11.2、private
定义为private
的field
、method
无法被其他类访问;
package abc;
public class Hello{
//不能被其他类调用
private void hi(){
}
public void hello(){
this.hi();
}
}
确切的说,private
访问权限被限定在class
的内部,而且与方法声明顺序无关
。推荐把private
方法放到后面,因为public
方法定义了类对外提供的方法,阅读代码的时候,应该先关注public
方法:
package bbb;
public class Hello{
public void hi(){
this.h();
}
private void h(){
}
}
由于Java支持嵌套类,如果一个类内部还定义了嵌套类。那么嵌套类具有访问private
的权限:
public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
}
//private方法:
private static void hello(){
System.out.println("private hello!");
}
//静态内部类
static class Inner{
public void hi(){
Main.hello();
}
}
}
定义在一个class
内部的class
成为嵌套类(nested class
),Java支持好几种嵌套类。
11.3、protected
protected
作用于继承关系。定义为protected
的字段和方法可以被子类、以及子类的子类访问:
package abc;
public class Hello{
// protected 方法
protected void hi(){
}
}
上面的protected
方法可以被继承的类访问;
package xxx;
class Main extends Hello{
void foo(){
// 可以访问protected 方法
hi();
}
}
11.4、package
最后,包作用域是指一个类允许访问同一个package
中没有public
、private
修饰的class
,以及没有public
、protected
、private
修饰的字段和方法。
package abc;
//package 权限的类
class Hello {
//package 权限的方法
void hi(){
}
}
只有在同一个包中,就可以访问package
的 class
、field
和method
:
package abc;
class Main{
void foo(){
/''
Hello h = new Hello();
h.hi()
}
}
注意,包名必须完全一致,包没有父子关系,
com.apache
和com.apache.abc
是不同的包。
11.5、局部变量
在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量
public class Hello {
void hi(String name) { // ①
String s = name.toLowerCase(); // ②
int len = s.length(); // ③
if (len < 10) { // ④
int p = 10 - len; // ⑤
for (int i=0; i<10; i++) { // ⑥
System.out.println(); // ⑦
} // ⑧
} // ⑨
} // ⑩
}
我们观察上面的hi()
方法代码:
- 方法参数name是局部变量,它的作用域是整个方法,即①~⑩;
- 变量s的作用域是定义处到方法结束,即②~⑩;
- 变量len的作用域是定义处到方法结束,即③~⑩;
- 变量p的作用域是定义处到if块结束,即⑤~⑨;
- 变量i的作用域是for循环,即⑥~⑧。
使用局部变量时,应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量。
11.6、final
Java还提供了一个final
修饰符。final
与访问权限不冲突,它有很多作用。
- 用
final
修饰class
可以阻止被继承:
package abc;
//无法被继承
public final class Hello {
private int n = 0;
protected void hi(int t){
long i = t;
}
}
- 用
final
修饰method
可以阻止被子类覆写:
package abc;
public class Hello {
//无法被覆写
protected final void hi(){
}
}
- 用
final
修饰field
可以阻止被重新赋值:
package abc;
public class Hello {
private final int n = 0;
protected void hi(){
this.n = 1; //error
}
}
- 用
final
修饰局部变量可以阻止被重新赋值:
package abc;
public class Hello {
protected void hi(final int t){
t = 1; //error
}
}
11.7、注意事项
如果不确定是否需要public
,就不声明为public
,尽可能少地暴露对外的字段和方法。
把方法定义为package
权限有助于测试,因为测试类和被测试类只要位于同一个package
,测试代码就可以访问被测试的package
权限方法。
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同。
十二、内部类
在Java程序中,通常情况下,我们把不同的类组织在不同的包下面,对于一个包下面的类来说,它们是在同一层次,没有父子关系:
java.lang
├── Math
├── Runnable
├── String
└── ...
还有一种类,它被定义在另一个类的内部,所以称为内部类(Nested Class)。Java的内部类分为好几种,通常情况用得不多,但也需要了解它们是如何使用的。
12.1、Inner Class
如果一个类定义在另一个类的内部,这个类就是Inner Class:
class Outer {
class Inner{
//定义了一个Inner Class
}
}
上面的Outer
是一个普通类,而Inner
是一个Inner Class
,它与普通类有个最大的不同,就是Inner Class的实例不能单独存在,必须依附于一个Outer Class的实例:
public class Main{
public static void main(String[] args) {
Outer o = new Outer("嵌套 "); //实例化一个Outer
Outer.Inner i = o.new Inner(); //实例化一个Inner
i.hello();
}
}
class Outer{
private String name;
Outer(String name){
this.name = name;
}
class Inner {
void hello(){
System.out.println("Hello, " + Outer.this.name)
}
}
}
观察上述代码,要实例化一个Inner
,我们必须首先创建一个Outer
的实例,然后调用Outer
实例的new
来创建Inner
实例:
Outer,Inner i = o.new Inner();
这是因为Inner Class除了有一个this
指向他自己,还隐含地持有一个Outer Class实例,可以用Outer.this
访问这个实例。所以,实例化一个Inner Class不能脱离Outer实例。
Inner Class 和普通Class相比,除了能引用Outer实例外,还有一个额外的“特权”,就是可以修改Outer Class的private
字段,因为Inner Class的作用域在Outer Class内部,所以能够访问Outer Class的private
字段和方法。
观察Java编译器编译后的.class
文件可以发现,Outer
类被编译为Outer.class
,而Inner
类被编译为Outer$Inner.class
。
12.2、Anonymous Class
在方法内部,通过匿名类(Anonymous Class)来定义
public class Main{
public static void main(String[] args){
Outer o = new Outer("嵌套");
o.asyncHello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello(){
Runnable r = new Runnable(){
@Override
public void run(){
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}
}
不太理解。。。。。
学习文档链接内部类 - 廖雪峰的官方网站 (liaoxuefeng.com)
12.3、Static Nested Class
静态内部类,和Inner Class类似,但使用static
修饰
public class Main{
public static void main(String[] args){
Outer.StaticNested sn = new Outer.StaticNested();
sn.hello();
}
}
class Outer {
private static String NAME = "OUTER";
private String name;
Outer(String name){
this.name = name;
}
static class StaticNested {
void hello(){
System.out.println("Hello, " + Outer.NAME);
}
}
}
用static
修饰的内部类和Inner Class有很大的不同,它不再依附于Outer
的实例,而是一个完全独立的类,因此无法引用Outer.this
,但它可以访问Outer
的private
静态字段和静态方法。如果把StaticNested
移到Outer
之外,就是去了访问private
的权限。
12.4、小结
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
访问权限。
十三、classpath和jar
详细教程classpath和jar - 廖雪峰的官方网站 (liaoxuefeng.com)
13.1、classpath
classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。
因为Java是编译型语言,源码文件是.java
,而编译后的.class
文件才是真正可以被JVM执行的字节码。因此JVM需要知道,如果要加载一个abc.xxx.Hello
的类,应该去哪里搜索对应的Hello.class
文件。
所以classpath
就是一组目录的合计,他始终的搜索路径与操作系统相关。
- Windows系统中,用
;
分隔,带空格的目录用""
括起来,如下:
C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"
- Linux系统中,用
:
分隔,如下:
/usr/shared:/usr/local/bin:/home/liaoxuefeng/bin
现在我们假设classpath
是.;C:\work\project1\bin;C:\shared
,当JVM在加载abc.xyz.Hello
这个类时,会依次查找:
- <当前目录>\abc\xyz\Hello.class
- C:\work\project1\bin\abc\xyz\Hello.class
- C:\shared\abc\xyz\Hello.class
注意到.
代表当前目录。如果JVM在某个路径下找到了对应的class
文件,就不再往后继续搜索。如果所有路径下都没有找到,就报错。
classpath
的设定方法有两种:
在系统环境变量中设置classpath
环境变量,不推荐;
在启动JVM时设置classpath
变量,推荐。
我们强烈不推荐在系统环境变量中设置classpath
,那样会污染整个系统环境。在启动JVM时设置classpath
才是推荐的做法。实际上就是给java
命令传入-classpath
或-cp
参数:
java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello
或者使用-cp
的简写:
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
没有设置系统环境变量,也没有传入-cp
参数,那么JVM默认的classpath
为.
,即当前目录:
java abc.xyz.Hello
不要把任何Java核心库添加到classpath中!JVM根本不依赖classpath加载核心库!
更好的做法是,不要设置
classpath
!默认的当前目录.
对于绝大多数情况都够用了
13.2、jar包
jar包可以把package
组织的目录层级,以及各个目录下的所有文件(包括.class
文件和其他文件)都打成一个jar文件,这样一来,无论是备份,还是发送都更简单。
jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class
,就可以把jar包放到classpath
中:
java -cp ./hello.jar abc.xyz.Hello
这样JVM就会自动在hello.jar
文件里去搜索某个类。
因为jar包就是zip包,直接右键将文件压缩为zip格式再重命名后缀为jar
即可
假设编译输出的目录结构是这样:
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
特别注意的是,jar包里的第一层目录,不能是
bin
,而应该是hong
、ming
、mr
。
//jar 包结构
package_sample
└─ bin
│
└─ hello.zip
│
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以指定Main-Class
和其它信息。JVM会自动读取这个MANIFEST.MF
文件,如果存在Main-Class
,我们就不必在命令行指定启动的类名,而是用更方便的命令:
java -jar hello.jar
13.3、小结
JVM通过环境变量classpath
决定搜索class
的路径和顺序;
不推荐设置系统环境变量classpath
,始终建议通过-cp
命令传入;
jar包相当于目录,可以包含很多.class
文件,方便下载和使用;
MANIFEST.MF
文件可以提供jar包的信息,如Main-Class
,这样可以直接运行jar包。
十四、模块
.class
文件时是JVM看到的最小可执行文件,而一个大型程序需要编写很多Class,并生成一堆.class
文件,不便于管理,所以,jar
文件就是class
文件的容器。
再Java 9 之前,JVM自带的Java标准库,实际上也是以jar文件形式存放的,这个文件叫rt.jar
,一共60多M。
如果自己开发的程序,除了一个自己的app.jar
以外,还需要一堆第三方的jar包,允许一个Java程序,一般来说命令行写这样:
java -cp app.jar:a.jar:b.jar:c.jar com.liaoxuefeng.sample.Main
注意:JVM自带的标准库rt.jar不要写到classpath中,写了反而会干扰JVM的正常运行。
如果漏写了某个运行时需要用到的jar,那么运行期极有可能抛出ClassNotFoundException
所以,jar只是用于存放class的容器,它并不关心class之间的额依赖
从Java 9 开始引入的模块,主要是为了解决依赖这个问题.如果a.jar
必须依赖另一个b.jar
才能运行,那我们应该给a.jar
加点说明,让程序再编译和运行的时候能自动定位到b.jar
,这种自带"依赖关系"的class容器就是模块。
从Java 9 开始,原有的Java标准库由一个巨大的rt.jar
拆分成几十个模块,以.jmod
扩展名标识。
这些.jmod
文件每一个都是一个模块,模块名就是文件名。例如:模块java.base
对应的文件就是java.base.mod
。模块之间的依赖关系以及被写入模块内的module-info.class
文件了。所有的模块都直接过间接的依赖java.base
模块,只有java.base
模块不依赖任何模块,它可以被看作"根模块",好比所有的类都是从Object
直接或间接继承而来。
把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。
14.1、编写模块
创建模块和原有的创建Java项目是完全一样的,以oop-module
工程为例,目录结构如下:
oop-module
├── bin
├── build.sh
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java
其中,bin
目录存放编译后的class文件,src
目录存放源码,按包名的目录结构存放,仅仅在src
目录下多了一个module-info.java
这个文件,这就是模块的描述文件:
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入java.base
requires java.xml;
}
其中,module
是关键字,后面的hello,world
是模块的名称,他的命名规范与包一致。花括号的requires xxx;
表示这个模块需要引用的其他模块名。除了java.base
可以被自动引入外,上方代码表示引入了一个java.xml
的模块。
使用模块声明了依赖关系后,才能使用引入的模块:
package com.itranswarp.sample;
// 必须引入java.xml模块后才能使用其中的类:
import javax.xml.XMLConstants;
public class Main {
public static void main(String[] args) {
Greeting g = new Greeting();
System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));
}
}
如果把requires java.xml;
从module-info.java
中去掉,编译将报错。可见,模块的重要作用就是声明依赖关系。
使用命令行工具来编译并创建模块
- 首先,把工作目录切换到
oop-module
,在当前目录下编译所有的.java
文件,并存放到bin
目录下,命令如下:
$ javac -d bin src/module-info.java src/com/itranswarp/sample/*.java
如果编译成功,现在项目结构如下:
oop-module
├── bin
│ ├── com
│ │ └── itranswarp
│ │ └── sample
│ │ ├── Greeting.class
│ │ └── Main.class
│ └── module-info.class
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java
注意到
src
目录下的module-info.java
被编译到bin
目录下的module-info.class
。
- 下一步,我们需要把bin目录下的所有class文件先打包成jar,在打包的时候,注意传入
--main-class
参数,让这个jar包能自己定位main
方法所在的类:
$ jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin .
- 现在我们就在当前目录下得到了
hello.jar
这个jar包,它和普通jar包并无区别,可以直接使用命令java -jar hello.jar
来运行它。但是我们的目标是创建模块,所以,继续使用JDK自带的jmod
命令把一个jar包转换成模块:
$ jmod create --class-path hello.jar hello.jmod
于是,在当前目录下我们又得到了hello.jmod
这个模块文件,这就是最后打包出来的传说中的模块!
14.2、运行模块
要运行一个jar,我们使用java -jar xxx.jar
命令。要运行一个模块,我们只需要指定模块名。试试:
$ java --module-path hello.jmod --module hello.world
结果是一个错误:
Error occurred during initialization of boot layer
java.lang.module.FindException: JMOD format not supported at execution time: hello.jmod
原因是.jmod
不能被放入--module-path
中。换成.jar
就没问题了:
$ java --module-path hello.jar --module hello.world
Hello, xml!
那我们辛辛苦苦创建的hello.jmod
有什么用?答案是我们可以用它来打包JRE。
14.3、打包JRE
前面讲了,为了支持模块化,Java 9首先带头把自己的一个巨大无比的rt.jar
拆成了几十个.jmod
模块,原因就是,运行Java程序的时候,实际上我们用到的JDK模块,并没有那么多。不需要的模块,完全可以删除。
过去发布一个Java应用程序,要运行它,必须下载一个完整的JRE,再运行jar包。而完整的JRE块头很大,有100多M。怎么给JRE瘦身呢?
现在,JRE自身的标准库已经分拆成了模块,只需要带上程序用到的模块,其他的模块就可以被裁剪掉。怎么裁剪JRE呢?并不是说把系统安装的JRE给删掉部分模块,而是“复制”一份JRE,但只带上用到的模块。为此,JDK提供了jlink
命令来干这件事。命令如下:
$ jlink --module-path hello.jmod --add-modules java.base,java.xml,hello.world --output jre/
我们在--module-path
参数指定了我们自己的模块hello.jmod
,然后,在--add-modules
参数中指定了我们用到的3个模块java.base
、java.xml
和hello.world
,用,
分隔。最后,在--output
参数指定输出目录。
现在,在当前目录下,我们可以找到jre
目录,这是一个完整的并且带有我们自己hello.jmod
模块的JRE。试试直接运行这个JRE:
$ jre/bin/java --module hello.world
Hello, xml!
要分发我们自己的Java应用程序,只需要把这个jre
目录打个包给对方发过去,对方直接运行上述命令即可,既不用下载安装JDK,也不用知道如何配置我们自己的模块,极大地方便了分发和部署。
14.4、访问权限
前面我们讲过,Java的class访问权限分为public、protected、private和默认的包访问权限。引入模块后,这些访问权限的规则就要稍微做些调整。
确切地说,class的这些访问权限只在一个模块内有效,模块和模块之间,例如,a模块要访问b模块的某个class,必要条件是b模块明确地导出了可以访问的包。
举个例子:我们编写的模块hello.world
用到了模块java.xml
的一个类javax.xml.XMLConstants
,我们之所以能直接使用这个类,是因为模块java.xml
的module-info.java
中声明了若干导出:
module java.xml {
exports java.xml;
exports javax.xml.catalog;
exports javax.xml.datatype;
...
}
只有它声明的导出的包,外部代码才被允许访问。换句话说,如果外部代码想要访问我们的hello.world
模块中的com.itranswarp.sample.Greeting
类,我们必须将其导出:
module hello.world {
exports com.itranswarp.sample;
requires java.base;
requires java.xml;
}
因此,模块进一步隔离了代码的访问权限。