JAVA之面向对象
JAVA之面向对象
什么是面向对象编程(OOP)?
面向对象编程(OOP)是一种通过对象的方式,将现实世界的概念映射到计算机模型的编程方法。与面向过程编程不同,面向对象编程注重对象之间的互动,而不是按步骤逐步执行。
面向对象的基本概念
- 类:定义对象的模板或蓝图。
- 实例:类的具体实现,称为对象。
- 方法:对象的行为或功能。
面向对象的实现方式
- 继承:子类继承父类的属性和方法。
- 多态:不同对象可以通过相同的接口以不同方式执行。
Java语言的机制
- package:组织类的工具。
- classpath:定义Java程序运行时所需的类路径。
- jar:将Java类和资源打包成单一文件的格式。
Java标准库的核心类
- 字符串:用于处理文本。
- 包装类型:将基本数据类型封装成对象。
- JavaBean:一种Java类标准,用于封装多个对象属性。
- 枚举:定义常量集合的特殊类。
- 常用工具类:提供各种实用功能的方法集合。
面向对象编程是一种通过“类”和“实例”来模拟现实世界的编程方法。简单来说,“类”是一个模板,用来创建对象,“实例”则是根据模板生成的具体对象。
现实世界与计算机模型的对应关系
- 现实世界: “人”是一个抽象概念,具体的“小明”、“小红”是实际的人。
- 计算机模型: “人”可以用类(class)表示,小明、小红等是实例(instance)。
对应的Java代码
- 定义“人”这个类:
1
class Person { }
- 创建“小明”、“小红”这两个实例:
1
2Person ming = new Person(); // 小明
Person hong = new Person(); // 小红
示例类:Book
让我们定义一个“书”的类(class),其中包含一些字段(field),如书名、作者、ISBN和价格:
1
2
3
4
5
6class Book {
public String name; // 书名
public String author; // 作者
public String isbn; // ISBN编号
public double price; // 价格
}
创建实例
定义好类后,可以用 new
创建实例。例如:
1
Book book1 = new Book(); // 创建一本书的实例
操作实例
通过实例变量操作实例的数据: 1
2book1.name = "Java核心技术"; // 设置书名
book1.author = "Cay S. Horstmann"; // 设置作者
示例 1: 定义和使用
Person
类
1 | class Person { |
输出: 1
2Xiao Ming is 12 years old.
Xiao Hong is 15 years old.
示例 2: 定义和使用 Book
类
1 | class Book { |
输出: 1
2Book 1: Java核心技术, Author: Cay S. Horstmann, Price: 99.0
Book 2: Java编程思想, Author: Bruce Eckel, Price: 129.0
示例 3: 使用多个实例
1 | class Person { |
输出: 1
Xiao Jun is reading Java学习笔记 by 某某作者
类方法
1. 定义类与字段
1 | class Person { |
将字段 name
和 age
定义为
private
,使外部无法直接访问。
2. 使用 getter
和
setter
方法
1 | public class Main { |
通过 getter
和 setter
方法控制
private
字段的访问,并在方法中加入逻辑检查。
3. 参数绑定
- 基本类型参数: 值复制,修改局部变量不影响对象字段。
- 引用类型参数: 指向同一个对象,修改引用会影响对象字段。
示例:
1 | public class Main { |
此示例展示了引用类型参数的绑定,修改数组内容会影响对象字段。
这个代码展示了引用类型参数传递的机制。
1. 引用类型参数的传递
在Java中,方法调用时的参数传递有两种情况:
- 基本数据类型(如
int
,float
,boolean
等):传递的是参数的值的副本,方法内修改副本不会影响原来的变量。- 引用数据类型(如数组,
String
,对象):传递的是对象的引用,方法内的修改会影响原来的对象,因为它们指向同一个内存地址。2.
setName()
方法的执行过程当你调用
p.setName(fullname)
时,fullname
数组的引用被传递给setName()
方法。在这个方法内部,这个引用被赋值给Person
类的name
字段:
1
2
3 public void setName(String[] name) {
this.name = name;
}此时,
this.name
和fullname
指向同一个数组对象。换句话说,p.name
和fullname
这两个变量都指向相同的内存地址。3. 修改数组的影响
接着,当你在
main
方法中修改数组fullname
的第一个元素:
1 fullname[0] = "Bart";因为
fullname
和p.name
都指向同一个数组对象,所以修改fullname
数组的内容会直接反映在p.name
中。因此,调用
getName()
时,输出结果会变成"Bart Simpson"
,而不是原来的"Homer Simpson"
。4. 代码输出分析
- 第一次调用
getName()
: 输出"Homer Simpson"
,因为在fullname
数组传递给setName()
时,数组内容还没有修改。- 第二次调用
getName()
: 输出"Bart Simpson"
,因为在调用setName()
后,我们修改了数组的第一个元素,而p.name
仍然指向同一个数组对象。引用类型参数的传递意味着两个变量可以指向同一个对象。通过这种方式,修改一个变量(比如数组元素)会影响到所有指向同一个对象的变量。因此,在操作引用类型数据时,理解这种共享引用机制至关重要。
4. 使用 this
关键字
1 | class Person { |
在字段与局部变量同名时,通过 this
关键字区分。
5. 可变参数
1 | class Group { |
可变参数允许传递多个参数,便于使用。
构造方法与初始化
构造方法用于在创建对象实例时初始化对象的内部状态。通过构造方法,我们可以确保在对象创建时内部字段被正确地初始化。以下是构造方法的基本实现:
1 | class Person { |
在创建Person
实例时,我们可以使用带参数的构造方法进行初始化,从而避免需要手动调用setName()
和setAge()
等方法。例如:
1 | public class Main { |
多个构造方法
我们可以定义多个构造方法,以便根据不同的需求初始化对象。例如,既可以创建无参数构造方法,也可以创建带参数的构造方法:
1 | class Person { |
在上面的代码中,通过调用this(…)
,一个构造方法可以复用另一个构造方法的逻辑,减少重复代码。
练习
请在Person
类中增加一个带参数的构造方法:
1 | public class Main { |
通过这样的构造方法,实例创建时的字段初始化更加简洁、可靠。
更加复杂的一些样例:
1. 多重初始化与对象嵌套
考虑一个Student
类,其中包含一个嵌套的Address
对象,这个Address
对象本身也需要初始化:
1 | class Address { |
在这个例子中,Student
类的构造方法不仅初始化了自身的字段,还初始化了一个Address
对象,展示了对象嵌套初始化的情况。
2. 构造方法的逻辑处理
在某些情况下,构造方法中需要进行复杂的逻辑处理。考虑一个BankAccount
类,初始化时需要处理余额、利率、账户类型等:
1 | class BankAccount { |
在这个例子中,构造方法不仅初始化了基本字段,还根据账户类型设置了不同的利率,展示了构造方法中的逻辑处理。
3. 构造方法中的参数校验
有时候,构造方法需要对传入的参数进行校验,以确保对象状态的有效性。考虑一个Employee
类,需要确保年龄在合理范围内:
1 | class Employee { |
在这个例子中,Employee
类的构造方法对年龄进行校验,如果传入的年龄不在合理范围内,将抛出异常,展示了如何在构造方法中进行参数校验。
4. 构造方法的重载与默认值
有时候,我们希望构造方法支持多种初始化方式,并且在某些情况下提供默认值。例如,一个Car
类可以通过多种方式初始化:
1 | class Car { |
在这个例子中,Car
类提供了多个构造方法,允许不同的初始化方式,并为未提供的参数设置了默认值。
方法重载 (Method Overloading)
方法重载是指在同一个类中,可以定义多个方法名相同但参数列表不同的方法。这些方法可以具有不同的参数个数、类型或顺序,但它们必须具有相同的方法名。这使得方法的调用更加灵活和易于记忆。
方法重载的规则
- 方法名相同:重载的方法必须有相同的方法名。
- 参数列表不同:重载的方法必须具有不同的参数列表。这包括参数的数量、类型或顺序。
- 返回类型:方法的返回类型不参与方法重载的匹配过程。换句话说,仅仅通过改变返回类型不能实现方法的重载。
示例
假设你有一个 Hello
类,你希望根据不同的参数形式提供不同的问候功能。你可以使用方法重载来实现这一点:
1 | class Hello { |
在上面的代码中,hello
方法被重载了三次,每次参数列表不同。这样,我们可以使用相同的方法名来处理不同的情况。
方法重载的实际应用
在 String
类中,也有许多重载的方法。例如,indexOf
方法用于查找子串的起始位置,但它提供了多种重载形式,以支持不同的查找需求:
1 | public class Main { |
解释
s.indexOf('t')
:查找字符't'
在字符串中的第一个出现位置。返回值是字符't'
的第一个索引,结果是0
。s.indexOf("st")
:查找子串"st"
在字符串中的第一个出现位置。结果是5
。s.indexOf("st", 4)
:从索引4
开始查找子串"st"
。结果是10
,因为从索引4
开始,第二次出现"st"
在位置10
。
方法重载使得同一个方法名可以处理不同类型的输入或不同的用例,使代码更加清晰和易于维护。
方法重载的深层次分析
1. 参数类型和顺序的细微区别
重载不仅仅依赖于参数的数量,还依赖于参数的类型和顺序。例如:
1 | class OverloadExample { |
在上面的代码中,process
方法被重载了三次,它们的参数类型和顺序不同:
process(int a)
:处理一个int
类型的参数。process(double a)
:处理一个double
类型的参数。process(int a, double b)
:处理一个int
和一个double
类型的参数。
2. 自动类型提升
Java 支持自动类型提升(auto-promotions),这意味着如果方法的参数类型不完全匹配,Java 会尝试将实参转换为目标方法所需的类型。这可能会导致选择不明确的情况。例如:
1 | class PromotionExample { |
调用 process(10)
时,10
是 int
类型,因此会调用 process(int a)
方法。然而,调用
process(10.5)
时,10.5
是 double
类型,因此会调用 process(double a)
方法。
但是,如果调用 process(10L)
(10L
是
long
类型),Java 会尝试将 long
类型提升为
double
类型,因为 long
类型无法直接匹配
int
类型的方法。结果是 process(double a)
方法会被调用。
3. 重载与构造函数
构造函数也可以重载。构造函数重载与普通方法的重载规则相同,只要构造函数的参数列表不同即可。例如:
1 | class Person { |
4. 重载与类型转换
有时方法的重载可能与类型转换相关。例如,如果有一个方法接受
Object
类型参数和一个方法接受具体类型参数,调用这些方法可能会受到自动类型转换的影响:
1 | class TypeConversionExample { |
调用 process("Hello")
会匹配
process(String str)
方法,因为 String
是
Object
的子类。调用 process(new Object())
会匹配 process(Object obj)
方法,因为 Object
是最通用的类型。
常见陷阱
- 方法签名:确保重载方法的签名(方法名和参数列表)是唯一的。返回类型不在签名中,因此不能仅通过改变返回类型来实现重载。
- 自动类型提升:注意自动类型提升可能导致的重载方法选择不明确。为了避免错误,尽量避免混用不同类型的参数。
- 调用模糊性:如果两个重载方法的参数列表差异不明显,可能会导致编译器无法确定调用哪个方法。尽量使方法的参数列表区别更明确,以减少这种问题。
继承和代码复用
在Java中,继承是一种让类重用另一个类的功能的机制。你可以通过extends
关键字实现继承,从而避免代码重复。
基本概念
继承的定义: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Person {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
class Student extends Person {
private int score;
public int getScore() { return score; }
public void setScore(int score) { this.score = score; }
}
解释: - Student
类通过
extends Person
继承了 Person
类的字段和方法。
- Student
不需要重复定义 name
和
age
字段,只需定义 score
相关的功能。
继承的特性
访问权限:
private
修饰的字段和方法不能被子类访问。protected
修饰的字段和方法可以被子类访问。
1
2
3
4
5
6
7
8
9class Person {
protected String name;
}
class Student extends Person {
public String getName() {
return name; // OK
}
}构造方法:
- 子类必须调用父类的构造方法。如果父类没有无参数的构造方法,子类构造方法中必须显式调用父类的构造方法。
1
2
3
4
5
6
7
8
9
10class Person {
public Person(String name, int age) { ... }
}
class Student extends Person {
public Student(String name, int age, int score) {
super(name, age); // 调用父类构造方法
this.score = score;
}
}super
关键字:- 用于引用父类的字段和方法。
- 在构造方法中,
super
用于调用父类的构造方法。
1
2
3
4
5
6class Student extends Person {
public Student(String name, int age, int score) {
super(name, age); // 调用父类构造方法
this.score = score;
}
}instanceof
操作符:- 检查一个对象是否是某个类的实例,或者是其子类的实例。
1
2
3
4Person p = new Student();
if (p instanceof Student) {
Student s = (Student) p; // 向下转型
}向上转型和向下转型:
- 向上转型:子类实例可以赋值给父类引用。
- 向下转型:父类引用可以强制转换为子类实例,需确保实际对象是子类实例。
1
2Person p = new Student(); // 向上转型
Student s = (Student) p; // 向下转型
继承与组合
继承:用于表示“is-a”关系(例如,
Student is a Person
)。组合:用于表示“has-a”关系(例如,
Student has a Book
)。1
2
3
4class Book { ... }
class Student extends Person {
private Book book; // 组合
}
例子:
复杂继承示例
示例 1:多层继承
场景:假设我们要构建一个动物分类系统,其中有不同的动物,每种动物都有自己的特性。
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 // 基类:动物
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating.");
}
}
// 子类:哺乳动物
class Mammal extends Animal {
public Mammal(String name) {
super(name);
}
public void walk() {
System.out.println(name + " is walking.");
}
}
// 子类:猫
class Cat extends Mammal {
public Cat(String name) {
super(name);
}
public void meow() {
System.out.println(name + " says meow.");
}
}
// 子类:狮子
class Lion extends Cat {
public Lion(String name) {
super(name);
}
public void eat() {
System.out.println(name + " is hunting.");
}
}
// 使用
public class Main {
public static void main(String[] args) {
Lion leo = new Lion("Leo");
leo.eat(); // Lion-specific behavior
leo.walk(); // Inherited from Mammal
leo.meow(); // Inherited from Cat
}
}解释: -
Lion
继承自Cat
,Cat
继承自Mammal
,Mammal
继承自Animal
。 -Lion
重写了eat()
方法,表现出狮子的特定行为。示例 2:接口与继承
场景:我们希望定义一个系统,涉及不同类型的可移动对象,例如车和飞行器。
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 // 接口:可移动
interface Movable {
void move();
}
// 类:车
class Car implements Movable {
private String model;
public Car(String model) {
this.model = model;
}
public void move() {
System.out.println(model + " is driving.");
}
}
// 类:飞行器
class Aircraft implements Movable {
private String model;
public Aircraft(String model) {
this.model = model;
}
public void move() {
System.out.println(model + " is flying.");
}
}
// 类:电动车(扩展了Car)
class ElectricCar extends Car {
public ElectricCar(String model) {
super(model);
}
public void charge() {
System.out.println(super.model + " is charging.");
}
}
// 使用
public class Main {
public static void main(String[] args) {
ElectricCar tesla = new ElectricCar("Tesla Model S");
tesla.move(); // 从Car继承
tesla.charge(); // 特有方法
Aircraft jet = new Aircraft("Jet");
jet.move(); // 从Aircraft实现的Movable接口
}
}解释: -
Movable
是一个接口,Car
和Aircraft
实现了它。 -ElectricCar
继承自Car
,继承了Car
的move()
方法,并增加了充电功能。接口(
interface
)在Java中是一种特殊的引用类型,用于定义类的行为规范。它允许我们声明方法,但不提供这些方法的具体实现。接口主要用于以下目的:
定义行为:接口可以定义一组方法,这些方法需要被实现类提供具体的行为。实现接口的类必须实现接口中的所有方法。
多重继承:Java不支持类的多重继承,但一个类可以实现多个接口,这种方式允许类继承多个行为特征。
实现多态:接口允许我们在程序中使用不同的实现类来处理相同的接口类型,从而实现多态性。
具体示例
1. 定义接口
1
2
3
4 // 定义一个接口 Movable
interface Movable {
void move(); // 接口中的方法没有方法体
}解释: -
interface
关键字用于定义接口。 -Movable
接口声明了一个move()
方法,但没有提供方法的具体实现。2. 实现接口
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
27 // 实现 Movable 接口的 Car 类
class Car implements Movable {
private String model;
public Car(String model) {
this.model = model;
}
public void move() {
System.out.println(model + " is driving.");
}
}
// 实现 Movable 接口的 Aircraft 类
class Aircraft implements Movable {
private String model;
public Aircraft(String model) {
this.model = model;
}
public void move() {
System.out.println(model + " is flying.");
}
}解释: -
Car
和Aircraft
类通过implements
关键字实现了Movable
接口。 - 这两个类都需要提供move()
方法的具体实现。3. 使用接口
1
2
3
4
5
6
7
8
9 public class Main {
public static void main(String[] args) {
Movable myCar = new Car("Toyota");
Movable myAircraft = new Aircraft("Boeing");
myCar.move(); // Output: Toyota is driving.
myAircraft.move(); // Output: Boeing is flying.
}
}解释: -
Movable
类型的变量myCar
和myAircraft
可以指向实现了Movable
接口的对象。 - 调用move()
方法时,根据对象的实际类型(Car
或Aircraft
),会执行相应的实现。示例 3:抽象类与多态
场景:创建一个图形绘制系统,使用抽象类来定义不同的图形。
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 // 抽象类:图形
abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
public abstract void draw();
}
// 子类:圆形
class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
public void draw() {
System.out.println("Drawing a " + color + " circle with radius " + radius);
}
}
// 子类:矩形
class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
public void draw() {
System.out.println("Drawing a " + color + " rectangle with width " + width + " and height " + height);
}
}
// 使用
public class Main {
public static void main(String[] args) {
Shape[] shapes = {
new Circle("red", 5.0),
new Rectangle("blue", 4.0, 6.0)
};
for (Shape shape : shapes) {
shape.draw(); // 多态
}
}
}解释: -
Shape
是一个抽象类,定义了一个抽象方法draw()
。 -Circle
和Rectangle
继承自Shape
并实现了draw()
方法。 - 使用多态,通过Shape
类型的数组来处理不同类型的图形。
抽象类和抽象方法
面向抽象编程(Programming to an Interface)是一种编程范式,它强调代码的设计与实现应基于抽象,而不是具体的实现细节。这种方法通过使用抽象类或接口,允许我们编写更加灵活和可扩展的代码。
1. 抽象类与抽象方法的基本概念
抽象类:抽象类是不能被实例化的类,它通常包含一个或多个抽象方法。抽象类为子类提供了一个统一的接口,并可以包含一些具体的方法实现。抽象类的主要目的是被继承,并强制子类实现其定义的抽象方法。
抽象方法:抽象方法是没有具体实现的方法,定义时使用
abstract
关键字。这些方法必须在子类中实现,否则子类本身也必须声明为抽象类。
1 | abstract class Animal { |
解释: - Animal
类是一个抽象类,其中
makeSound()
是抽象方法,必须由子类实现。 -
sleep()
是一个具体方法,子类可以直接继承使用。
2. 子类继承抽象类并实现抽象方法
当一个子类继承抽象类时,必须实现所有的抽象方法。如果没有实现,子类也必须声明为抽象类。
1 | class Dog extends Animal { |
解释: - Dog
和 Cat
继承了
Animal
,并各自实现了 makeSound()
方法。 -
这样,每个子类都有了特定的行为,但在使用上依然可以统一为
Animal
类型处理。
3. 面向抽象编程的好处
统一接口,灵活实现:在面向抽象编程中,我们可以使用抽象类或接口类型的变量来引用不同的子类对象,从而调用子类的实现方法。这样做的好处是,代码的调用者不需要了解具体的子类,只需知道接口或者抽象类的定义即可。
1 | public class Main { |
解释: - 在 Main
类中,我们使用了
Animal
类型来声明变量 myDog
和
myCat
,但实际引用了具体的 Dog
和
Cat
对象。 -
这样,我们在使用时并不关心对象的具体类型,只需调用
makeSound()
方法,即可得到正确的行为。
4. 面向抽象编程的扩展性
由于面向抽象编程依赖于抽象类或接口定义行为规范,而不依赖于具体实现,所以代码具有很好的扩展性。当需要增加新的行为或修改现有行为时,可以通过新增子类或实现新的接口来完成,而无需修改现有代码。
扩展示例:
1 | class Bird extends Animal { |
1 | public class Main { |
解释: - 新增 Bird
类并实现
makeSound()
方法,扩展了 Animal
的行为。 -
由于 Main
类的代码只依赖于抽象的 Animal
类型,因此无需对 Main
类进行任何修改,就可以使用新的
Bird
类。
5. 面向抽象编程的应用场景
多态性:通过面向抽象编程,可以实现多态性,即同一操作在不同的对象上可能表现出不同的行为。例如,makeSound()
在 Dog
和 Cat
中表现不同。
解耦合:将具体实现与使用代码分离,通过依赖抽象类或接口,可以减少代码的耦合度,便于后续的维护和扩展。
增强可测试性:在单元测试中,通过面向抽象编程可以轻松地使用不同的子类或模拟对象(Mock Object)来测试代码的不同部分,从而提高代码的可测试性。
6. 实现与接口的结合
在实际应用中,接口(interface
)常与抽象类结合使用。接口定义行为规范,而抽象类可以提供部分实现,子类通过实现接口并继承抽象类来实现具体的业务逻辑。
1 | interface Movable { |
解释: - Movable
接口定义了
move()
方法。 - Vehicle
抽象类实现了
Movable
接口,并添加了 fuel()
抽象方法。 -
Car
和 Bicycle
类继承了 Vehicle
并实现了 move()
和 fuel()
方法,提供了具体的行为。
通过这种设计,程序可以更灵活地适应不同的业务需求,并通过面向抽象编程实现高扩展性和可维护性。
接口
在Java中,抽象类和接口的设计分别代表不同的抽象程度,并在不同场景下发挥其作用。为了更好地理解它们的区别以及如何合理使用,我们可以详细分析各自的特点及应用场景。
抽象类与接口的对比
特性 | 抽象类(abstract class ) |
接口(interface ) |
---|---|---|
继承方式 | 只能继承一个抽象类 | 可以实现多个接口 |
字段 | 可以包含实例字段 | 不能包含实例字段 |
方法 | 可以定义抽象方法和非抽象方法 | 可以定义抽象方法和默认方法 (default method ) |
构造器 | 可以有构造器 | 不能有构造器 |
使用场景 | 用于共享代码和定义类的基础行为 | 用于定义类的行为规范 |
继承关系 | 适合在类之间建立具体的继承层次关系 | 适合在不同的类之间建立共同的行为接口 |
接口继承与扩展
在Java中,一个接口可以继承另一个接口,这种继承关系通过
extends
关键字实现。接口继承其他接口后,可以扩展接口的方法。例如:
1 | interface Hello { |
在这个例子中,Person
接口继承了 Hello
接口,这意味着 Person
接口中包含了 Hello
接口定义的 hello()
方法。任何实现 Person
接口的类都需要实现 run()
、getName()
和
hello()
三个方法。
面向接口编程的优势
面向接口编程是一种设计原则,旨在通过使用接口来定义系统的行为规范,使得上层代码不依赖于具体的实现类,而是依赖于接口。这样做的优势包括:
提高代码的灵活性:上层代码只需依赖接口,而不需要知道具体的实现类,这使得代码可以更容易地进行扩展和修改。
增强代码的可测试性:由于接口定义了行为规范,可以通过传入不同的实现类来测试代码的不同行为。
实现多态性:接口可以使得不同的类表现出相同的行为(即多态性),从而提高代码的重用性。
default
方法与抽象类的普通方法
Java 8引入了接口中的 default
方法,允许接口提供默认的实现而不要求子类必须覆写。使用
default
方法可以有效减少子类的代码重复,尤其是在接口需要扩展新功能时。例如:
1 | interface Person { |
在这个例子中,Student
类没有覆写 run()
方法,它直接继承了接口中的 default
实现。
与抽象类的普通方法相比,default
方法有一些限制: -
不能访问实例字段:接口中没有字段,因此
default
方法不能访问实现类中的字段,而抽象类的普通方法可以访问实例字段。 -
多重继承冲突:如果一个类实现了多个接口,这些接口有相同的
default
方法签名时,会产生冲突,需要显式地覆写该方法来解决冲突。
对限制一进行举例:
1. 接口中的
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
27
28
29 interface Person {
String getName();
default void introduce() {
// 尝试访问 name 字段(会失败)
// System.out.println("Hello, my name is " + name); // 编译错误,因为接口中没有字段
System.out.println("Hello, my name is " + getName());
}
}
class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
public class Main {
public static void main(String[] args) {
Student student = new Student("Alice");
student.introduce(); // 调用接口中的 default 方法
}
}解释: - 在
Person
接口中,introduce()
是一个default
方法。它不能直接访问实现类Student
中的name
字段,因为接口不能包含实例字段。 - 但是,introduce()
可以通过调用getName()
方法来获取name
的值,因为getName()
是由实现类Student
提供的。输出:
1 Hello, my name is Alice2. 抽象类中的普通方法
与接口不同,抽象类中的普通方法可以直接访问实例字段。来看以下代码示例:
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
27
28
29
30
31
32
33 abstract class Person {
protected String name;
public Person(String name) {
this.name = name;
}
public abstract String getName();
public void introduce() {
// 可以直接访问 name 字段
System.out.println("Hello, my name is " + name);
}
}
class Student extends Person {
public Student(String name) {
super(name);
}
public String getName() {
return this.name;
}
}
public class Main {
public static void main(String[] args) {
Student student = new Student("Bob");
student.introduce(); // 调用抽象类中的普通方法
}
}解释: - 在
Person
抽象类中,introduce()
是一个普通方法,它直接访问了抽象类的name
字段。 -Student
类继承了Person
抽象类,并通过构造器初始化了name
字段。输出:
1 Hello, my name is Bob
解释一下限制二:
当一个类实现了多个接口,这些接口有相同的
default
方法签名时,会产生多重继承冲突。为了避免这种冲突,Java要求在实现类中显式地覆写冲突的default
方法。下面是一个代码示例来解释这个限制。示例代码
假设我们有两个接口
Person
和Worker
,它们都定义了一个相同签名的default
方法greet()
:
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
27
28
29
30
31
32 interface Person {
default void greet() {
System.out.println("Hello from Person!");
}
}
interface Worker {
default void greet() {
System.out.println("Hello from Worker!");
}
}
class Employee implements Person, Worker {
public void greet() {
// 显式指定要调用哪个接口的default方法,或者定义自己的实现
// 例如,调用Person接口的greet:
Person.super.greet();
// 或者调用Worker接口的greet:
// Worker.super.greet();
// 也可以自定义实现:
// System.out.println("Hello from Employee!");
}
}
public class Main {
public static void main(String[] args) {
Employee e = new Employee();
e.greet(); // 调用解决冲突后的greet方法
}
}解释
- 接口定义:
Person
和Worker
接口都定义了一个相同的default
方法greet()
,分别输出不同的消息。- 冲突产生:
Employee
类实现了Person
和Worker
两个接口。由于这两个接口都有相同签名的default
方法greet()
,Java 无法自动决定应该使用哪个greet()
方法,这就导致了多重继承冲突。- 解决冲突:
- 在
Employee
类中,必须显式地覆写greet()
方法来解决冲突。在覆写的方法内部,可以选择调用Person.super.greet()
或Worker.super.greet()
,也可以定义自己的实现。- 例如,在
greet()
方法中,调用了Person.super.greet()
来使用Person
接口的greet()
方法。输出结果
如果运行上述代码,输出将是:
1 Hello from Person!
- 当实现多个接口时,如果这些接口有相同签名的
default
方法,Java 会强制要求在实现类中显式覆写该方法来解决冲突。- 在覆写的方法中,可以选择调用其中一个接口的
default
方法,或者定义一个新的实现来解决冲突。
静态字段 (Static Field)
在Java中,静态字段是属于类本身的,而不是某个实例。当一个字段被
static
修饰时,这个字段在所有实例中都是共享的。无论有多少个实例,静态字段只存在一份,并且它的值对所有实例都是相同的。
示例解释
1 | class Person { |
在上面的代码中,name
和 age
是实例字段,而
number
是静态字段。实例字段 name
和
age
是每个 Person
实例独有的,而静态字段
number
是所有 Person
实例共享的。
来看一个实例使用静态字段的例子:
1 | public class Main { |
解释:
- 当
ming.number = 88;
时,静态字段number
的值被设置为88
。 - 当
hong.number
被打印时,输出的是88
,因为number
是静态的,所有实例共享同一个值。 - 当
hong.number = 99;
时,静态字段number
被修改为99
。 - 再次打印
ming.number
时,输出的是99
,因为ming
和hong
实际上共享同一个number
字段。
关键点: -
静态字段与实例无关,而是与类绑定的。所有实例共享一个静态字段。 -
通常,我们推荐使用 类名.静态字段
来访问静态字段,避免混淆。
静态方法 (Static Method)
静态方法是用 static
修饰的方法,属于类本身,而不是类的实例。调用静态方法不需要实例化类,可以直接使用
类名.静态方法
来调用。
示例代码
1 | class Person { |
解释:
setNumber
方法是一个静态方法,它设置了number
字段的值。setNumber
方法直接通过类名Person
调用,而不需要创建Person
的实例。
静态方法的限制:
- 不能访问实例字段:
因为静态方法不依赖于任何实例,所以它不能访问
this
关键字,也不能访问实例字段。它只能访问静态字段和静态方法。 - 通常用于工具类和辅助方法:
静态方法通常用于不依赖实例的数据操作,如数学运算
(
Math.random()
) 或数组操作 (Arrays.sort()
)。
接口的静态字段
在Java中,接口 (interface
)
是一种特殊的抽象类。接口不能包含实例字段,但可以定义静态字段,并且这些字段必须是
final
类型的常量。
示例代码
1 | public interface Person { |
解释:
- 在接口中,字段默认是
public static final
类型,表示它们是全局常量。你可以直接通过接口名.常量名
来访问这些常量。 - 这些常量可以用于表示固定的值,如性别分类等。
- 静态字段 是属于类的,可以被所有实例共享。
- 静态方法 也是属于类的,不需要实例化就能调用,但不能访问实例字段。
- 接口中的静态字段 是全局常量,用来表示固定的值,它们是
public static final
类型。
包
在Java中,包(package
)用于组织和管理类,使它们在命名上互不冲突。通过定义包,我们可以避免类名冲突,并且更好地组织代码结构,尤其是在大型项目中。
1. 包的定义和使用
1.1 定义包
在编写Java类时,我们可以在文件的第一行通过package
语句来定义该类所属的包。例如:
1 | package com.example.mypackage; |
这里,com.example.mypackage
就是包名,表示MyClass
类属于这个包中。
1.2 包名的结构
包名通常采用倒置域名的形式。例如,域名example.com
可以被倒置为com.example
,然后在其下根据项目需求进一步细分。例如:
com.example.util
:工具类包。com.example.model
:模型类包。
这种命名方式可以有效避免包名冲突,因为域名在全球是唯一的。
1.3 包与类的关系
类的完整名称是包名.类名
。例如,如果我们有一个类Person
在com.example.mypackage
包中,它的完整类名就是com.example.mypackage.Person
。
Java虚拟机(JVM)在执行时使用完整的类名来区分类。因此,只要包名不同,即使类名相同,它们也被视为不同的类。
2. 文件目录结构
Java文件的目录结构应与包结构对应。假设我们有如下的包和类:
- 包:
com.example.mypackage
- 类:
MyClass
则对应的文件路径应为:
1 | src/com/example/mypackage/MyClass.java |
编译后生成的.class
文件应存放在对应的目录中:
1 | bin/com/example/mypackage/MyClass.class |
3. 包作用域
在Java中,访问控制符包括public
、protected
、private
和包作用域(默认访问权限)。如果一个类的成员(字段或方法)没有被public
、protected
或private
修饰,它就是包作用域。
包作用域的成员只能被同一个包中的其他类访问。例如:
1 | package com.example; |
4. import语句
在一个类中,我们经常需要引用其他包中的类。这时可以通过import
语句导入这些类,来避免每次使用时都写完整类名。
4.1 单个类的导入
1 | import com.example.util.MyUtilityClass; |
4.2 导入整个包
1 | import com.example.util.*; |
这种写法导入了com.example.util
包下的所有类(但不包括子包中的类),虽然方便,但不推荐使用,因为可能导致名称冲突,难以明确具体引用的是哪个类。
4.3 静态导入
静态导入允许我们导入某个类的静态成员(字段或方法),从而可以直接使用这些静态成员而无需类名。例如:
1 | import static java.lang.Math.*; |
5. Java编译和运行
5.1 编译源码
假设我们有如下的项目目录结构:
1 | work |
编译所有的Java文件,并指定输出目录为bin
:
1 | javac -d ./bin src/**/*.java |
-d ./bin
:指定编译输出的.class文件存放在bin
目录中。src/**/*.java
:编译src
目录及其子目录下的所有.java
文件。
注意:Windows下不支持**
这种通配符,需要逐个指定Java文件进行编译。
5.2 运行程序
编译完成后,类文件将按照包结构存放在bin
目录中,例如:
1 | bin/com/example/util/MyUtilityClass.class |
运行Main
类:
1 | java -cp bin com.example.main.Main |
其中,-cp bin
指定了classpath
,即Java运行时需要搜索的类路径,com.example.main.Main
是我们要运行的类的完整类名。
6. 最佳实践
6.1 使用唯一的包名
为了避免名字冲突,建议使用倒置的域名来作为包名前缀。例如:
org.apache.commons.logging
:Apache Commons Logging包。com.mycompany.myapp
:你自己的应用包。
6.2 避免与JDK类重名
避免使用与JDK标准库中类重名的类名,例如:
String
System
List
Format
6.3 避免与常用类重名
避免使用与常用库(如Apache Commons、Google Guava等)中的类重名的类名。
包的应用
1. 基本包的使用
假设我们有两个开发者:小明和小红,他们各自开发了自己的类,分别用来表示一个人(Person
类)。为了避免类名冲突,他们将各自的 Person
类放在不同的包中。
小明的代码:
目录结构: 1
2
3
4project/
└── src/
└── ming/
└── Person.java
Person.java
(位于 ming
包): 1
2
3
4
5
6
7package ming;
public class Person {
public void sayHello() {
System.out.println("Hello from Ming's Person!");
}
}
小红的代码:
目录结构: 1
2
3
4project/
└── src/
└── hong/
└── Person.java
Person.java
(位于 hong
包): 1
2
3
4
5
6
7package hong;
public class Person {
public void sayHello() {
System.out.println("Hello from Hong's Person!");
}
}
小白想要使用小明和小红的
Person
类:
Main.java
(位于 main
包):
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main;
import ming.Person as MingPerson;
import hong.Person as HongPerson;
public class Main {
public static void main(String[] args) {
MingPerson mingPerson = new MingPerson();
HongPerson hongPerson = new HongPerson();
mingPerson.sayHello();
hongPerson.sayHello();
}
}
输出结果: 1
2Hello from Ming's Person!
Hello from Hong's Person!
通过将 Person
类分别放在 ming
和
hong
包中,小白能够同时使用这两个类,而不会发生类名冲突。
2. 使用 import
导入类
在很多情况下,直接使用类的完整路径会显得冗长,这时我们可以使用
import
来简化代码。
不使用 import
的方式:
1
2
3
4
5
6
7
8
9
10
11package main;
public class Main {
public static void main(String[] args) {
ming.Person mingPerson = new ming.Person();
hong.Person hongPerson = new hong.Person();
mingPerson.sayHello();
hongPerson.sayHello();
}
}
使用 import
的方式: 1
2
3
4
5
6
7
8
9
10
11
12
13
14package main;
import ming.Person;
import hong.Person;
public class Main {
public static void main(String[] args) {
Person mingPerson = new Person(); // 会出现冲突
Person hongPerson = new Person(); // 会出现冲突
mingPerson.sayHello();
hongPerson.sayHello();
}
}
当包名相同时(如上面的 ming.Person
和
hong.Person
),我们不能简单地使用
import
,需要使用完整类名或为类起别名。
3. 包结构的实际应用
在实际开发中,我们通常会将项目划分为多个子包,以便组织代码。比如开发一个在线商店系统,我们可以将不同功能模块的代码放在不同的包中。
目录结构: 1
2
3
4
5
6
7
8
9
10project/
└── src/
└── com/
└── shop/
├── customer/
│ └── Customer.java
├── order/
│ └── Order.java
└── product/
└── Product.java
Customer.java
(位于
com.shop.customer
包): 1
2
3
4
5
6
7
8
9
10
11
12
13package com.shop.customer;
public class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Order.java
(位于 com.shop.order
包): 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.shop.order;
import com.shop.customer.Customer;
public class Order {
private Customer customer;
public Order(Customer customer) {
this.customer = customer;
}
public void printOrder() {
System.out.println("Order for customer: " + customer.getName());
}
}
Product.java
(位于 com.shop.product
包): 1
2
3
4
5
6
7
8
9
10
11
12
13package com.shop.product;
public class Product {
private String name;
public Product(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Main.java
(位于 com.shop.main
包): 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.shop.main;
import com.shop.customer.Customer;
import com.shop.order.Order;
import com.shop.product.Product;
public class Main {
public static void main(String[] args) {
Customer customer = new Customer("Alice");
Product product = new Product("Laptop");
Order order = new Order(customer);
order.printOrder();
}
}
通过这种包结构,代码得到了很好的组织,方便管理和维护。不同的模块功能被清晰地分离到不同的包中,避免了名字冲突,并增强了代码的可读性。
4. 使用 import static
有时我们希望直接使用一个类的静态方法或字段,而不希望每次都写类名。这时可以使用
import static
。
Main.java
(使用
import static
): 1
2
3
4
5
6
7
8
9package main;
import static java.lang.System.out;
public class Main {
public static void main(String[] args) {
out.println("Hello, world!"); // 直接使用静态方法 System.out.println
}
}
通过 import static
,可以直接调用
System.out.println
,使代码更加简洁。
访问修饰符
在 Java
中,访问修饰符(public
、protected
、private
和
包级别权限)用于控制类、方法和字段的可访问性。这些修饰符的使用可以帮助我们定义类的外部接口、保护内部实现,并且控制代码的可见性和使用范围。
1. public
public
访问修饰符意味着这个类、方法或字段对所有其他类都是可见的,无论它们位于哪个包中。
应用场景
- 类级别:如果你希望一个类在任何包中都可以被访问,则将其声明为
public
。 - 方法和字段:通常用于定义类对外公开的
API。比如,如果某个方法或字段需要被类的外部调用或访问(例如工具类中的方法),它们通常会被声明为
public
。
示例:
1 | package com.example; |
在这个例子中,Greeting
类以及它的 message
字段和 sayHello()
方法都是 public
的,因此任何其他类都可以创建 Greeting
类的实例,并调用
sayHello()
方法。
2. private
private
访问修饰符意味着这个方法或字段只能在它所在的类内部被访问,不能被任何其他类访问。
应用场景
- 字段:通常用于保护类的内部状态,防止外部直接修改类的字段。
- 方法:用于封装类的内部逻辑,只在类的内部使用,不希望被外部调用。
示例:
1 | package com.example; |
在这个例子中,balance
字段是 private
的,意味着它只能在 BankAccount
类的内部被访问。外部代码无法直接修改 balance
,必须通过
deposit()
方法来更新余额。
3. protected
protected
访问修饰符意味着这个字段或方法可以被同一个包中的其他类访问,也可以被任何继承它的子类访问。
应用场景
- 继承:如果你希望类的某些字段或方法只对子类可见,但不希望对外部完全公开,可以使用
protected
。
示例:
1 | package com.example; |
在这个例子中,name
字段和 makeSound()
方法是 protected
的,因此可以在 Animal
类的子类 Dog
中访问。
4. 包级别权限(无修饰符)
如果你不为类、方法或字段指定任何访问修饰符,它们将具有包级别权限(即 “default” 访问级别)。这种权限意味着它们只能被同一个包中的其他类访问。
应用场景
- 内部实现:当你希望类或成员变量仅在包内部被访问,而不需要对包外部公开时,可以使用包级别权限。这在包内封装复杂逻辑,但仍允许包内其他类互相协作时非常有用。
示例:
1 | package com.example; |
在这个例子中,PackageClass
类和
showMessage()
方法具有包级别权限,因此 Main
类可以访问它们,但 com.example
包之外的类无法访问。
5. final
final
修饰符可以用来修饰类、方法和字段,它有以下作用: -
类:表示该类不能被继承。 -
方法:表示该方法不能被子类重写。 -
字段:表示该字段的值在初始化后不能被修改(成为常量)。
- 局部变量:表示该变量的值在初始化后不能被修改。
应用场景
- 不可变类:
final
类用于定义不可变的类,确保类的行为不会被子类改变。 - 常量:
final
字段通常与static
一起使用,用来定义常量。 - 方法优化:将方法声明为
final
可以让编译器进行优化,因为它知道该方法不会被重写。
示例:
1 | package com.example; |
在这个例子中,Constants
类是 final
的,表示它不能被继承。PI
字段是 static final
的,表示它是一个常量。在 Circle
类中,radius
字段也是 final
的,因此一旦被赋值就不能被更改。
最佳实践
- 最小暴露原则:如果不确定是否需要
public
,则尽量少使用public
,减少类和成员对外的暴露。 - 封装内部实现:使用
private
来封装类的内部实现细节,保护类的内部状态。 - 继承与可访问性:使用
protected
来定义类对继承的子类开放的 API,但不对外部完全公开。 - 包内协作:使用包级别权限来限制访问权限在包内,帮助包内类之间的协作。
举例:
1. public
修饰符的应用
场景: 创建一个工具类,提供通用的字符串操作方法。
示例: 1
2
3
4
5
6
7
8
9package com.utils;
public class StringUtils {
// 公开的方法,任何类都可以访问
public static String reverse(String input) {
StringBuilder sb = new StringBuilder(input);
return sb.reverse().toString();
}
}
应用场景说明: - StringUtils
类及其
reverse()
方法被声明为
public
,这样任何包中的类都可以使用这个方法来反转字符串。 -
这是典型的工具类设计,工具类中的方法通常被声明为
public
,因为它们需要对外提供广泛的功能。
2. private
修饰符的应用
场景: 在银行账户类中保护账户余额,防止外部直接修改。
示例: 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
27
28
29package com.bank;
public class BankAccount {
// 私有字段,外部类无法直接访问或修改
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
// 公开的存款方法
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount;
}
}
// 公开的取款方法
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
this.balance -= amount;
}
}
// 公开的查询余额方法
public double getBalance() {
return this.balance;
}
}
应用场景说明: - balance
字段被声明为
private
,保护了账户的内部状态,使得外部类不能直接修改余额,避免数据不一致的风险。
- 外部类只能通过 deposit()
、withdraw()
和
getBalance()
方法来操作或查询余额,这样可以确保每次余额的变化都通过合法的业务逻辑进行。
3. protected
修饰符的应用
场景: 创建一个动物类,允许子类定义各自的叫声,但不允许外部直接调用。
示例: 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
27
28
29
30
31
32
33
34package com.zoo;
public class Animal {
protected String name;
// 受保护的构造方法,防止直接实例化,但允许子类调用
protected Animal(String name) {
this.name = name;
}
// 受保护的方法,子类可以调用,但外部类不可以
protected void makeSound() {
System.out.println(name + " makes a sound");
}
}
class Dog extends Animal {
public Dog() {
super("Dog");
}
// 子类可以调用受保护的方法
protected void makeSound() {
System.out.println(name + " says: Woof!");
}
}
class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound(); // 调用的是Dog类中的makeSound方法
}
}
应用场景说明: - Animal
类的构造方法和
makeSound()
方法被声明为 protected
,这样
Animal
类无法被直接实例化,但可以被继承,并允许子类如
Dog
自定义 makeSound()
方法的行为。 - 外部类
Main
无法直接调用 Animal
类中的
makeSound()
方法,但可以通过 Dog
类的实例调用该方法。
4. 包级别权限(无修饰符)的应用
场景: 在一个包中创建多个类,某些类只在包内使用,不希望暴露给包外的类。
示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.example;
// 包级别权限类,只能在com.example包内访问
class PackageClass {
void display() {
System.out.println("This is a package-private method");
}
}
public class Main {
public static void main(String[] args) {
PackageClass pc = new PackageClass();
pc.display(); // 在同一个包内,可以访问display方法
}
}
应用场景说明: - PackageClass
类及其
display()
方法都没有任何修饰符,因此它们具有包级别权限,表示它们只能在
com.example
包内被访问。 -
这种设计用于包内协作,允许包内的类互相访问和使用,但避免了包外部类的直接访问,保持包的封装性。
5. final
修饰符的应用
场景: 创建一个不可变的类,防止子类继承或修改类中的字段。
示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package com.constants;
// 定义一个不可变的类
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
应用场景说明: - ImmutablePoint
类被声明为 final
,表示它不能被继承,确保了类的不可变性。 -
类中的 x
和 y
字段也被声明为
final
,它们只能在构造函数中被赋值,并且之后不能再被修改,确保实例一旦创建就无法改变状态。
内部类
Java中的内部类是一种非常有用的结构,用于在类的内部定义其他类。内部类的主要类型包括普通内部类(Inner Class)、匿名类(Anonymous Class)、静态内部类(Static Nested Class)。
1. 普通内部类(Inner Class)
定义:
一个类定义在另一个类的内部,这个内部类与外部类有很强的关联。它能够访问外部类的所有成员,包括private
成员。
特性: -
内部类的实例依附于外部类的实例,不能独立存在。 -
内部类可以直接访问外部类的private
字段和方法。 -
编译后的内部类文件名通常为Outer$Inner.class
。
例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
System.out.println("Hello, " + Outer.this.name);
}
}
}
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
Outer.Inner inner = outer.new Inner(); // 必须通过outer.new创建Inner实例
inner.hello(); // 输出: Hello, Nested
}
}
在这个例子中,Inner
类依赖于Outer
类的实例,并且可以访问Outer
类的私有字段name
。
2. 匿名类(Anonymous Class)
定义: 匿名类是在没有显式定义类名的情况下,在方法内部通过实例化一个接口或类时定义的类。通常用于简化代码,不需要重复创建单独的类文件。
特性: -
通常用来快速实现接口或继承类,特别是在需要简单实现的场景下。 -
可以访问外部类的private
字段和方法。 -
编译后的匿名类文件通常命名为Outer$1.class
、Outer$2.class
等。
例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() {
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}
}
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Anonymous");
outer.asyncHello(); // 异步输出: Hello, Anonymous
}
}
在这个例子中,我们使用匿名类实现了Runnable
接口,并通过线程执行run
方法。在没有显式定义类名的情况下创建了一个实现Runnable
的类。
1. 外部类
Outer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() {
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}
}
name
字段:Outer
类中定义了一个私有字段name
,用于存储传入的名字。构造方法
Outer(String name)
:这个构造方法用于初始化Outer
对象,并为name
字段赋值。
asyncHello
方法:这是Outer
类中的一个方法,用于异步地输出一个问候语。这个方法展示了匿名类的使用。2. 匿名类
Runnable
1
2
3
4
5
6 Runnable r = new Runnable() {
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
Runnable
接口:Runnable
是一个接口,里面定义了一个方法run()
,该方法在新线程中执行任务。匿名类实现
Runnable
:在asyncHello
方法内部,我们创建了一个实现Runnable
接口的匿名类。在这个匿名类中,重写了run()
方法。run()
方法中,我们使用了System.out.println()
打印问候语,并且引用了Outer.this.name
来访问外部类Outer
的name
字段。3. 线程启动
1 new Thread(r).start();
创建线程:我们将上面创建的匿名类实例
r
传入Thread
的构造方法中,这样Thread
对象就知道在新线程中执行r
的run()
方法。启动线程:
start()
方法启动了新线程,并执行匿名类中的run()
方法。在这个方法中,我们打印了Hello,
加上Outer
实例的name
。4. 主类
Main
1
2
3
4
5
6 public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Anonymous");
outer.asyncHello(); // 异步输出: Hello, Anonymous
}
}
main
方法:这是程序的入口。在main
方法中,我们创建了Outer
类的实例,并传入了字符串"Anonymous"
。调用
asyncHello
:我们调用了outer
对象的asyncHello()
方法,这将启动一个新线程,在新线程中执行打印操作。5. 执行过程
当
asyncHello()
被调用时,一个新线程被启动。这个新线程执行匿名类的run()
方法,打印出Hello, Anonymous
。因为使用了新线程,打印操作是异步执行的,这意味着它可能会在
main
方法结束后才执行。这就是“异步输出”的含义。
3. 静态内部类(Static Nested Class)
定义:
静态内部类与普通内部类不同,它用static
修饰,因此不依赖外部类的实例。它与外部类的实例没有直接关联,只能访问外部类的静态成员。
特性: - 可以直接实例化,而无需外部类的实例。 -
不能访问外部类的非静态成员,只能访问静态成员。 -
编译后的静态内部类文件通常命名为Outer$StaticNested.class
。
例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Outer {
private static String NAME = "OUTER";
static class StaticNested {
void hello() {
System.out.println("Hello, " + Outer.NAME);
}
}
}
public class Main {
public static void main(String[] args) {
Outer.StaticNested nested = new Outer.StaticNested();
nested.hello(); // 输出: Hello, OUTER
}
}
在这个例子中,StaticNested
是Outer
的静态内部类,可以直接实例化并访问Outer
的静态字段NAME
。
总结
- 普通内部类:依附于外部类实例,能访问外部类的所有成员,适用于需要紧密耦合的类。
- 匿名类:简化实现接口或继承类的代码,适用于简单、一次性的类定义。
- 静态内部类:独立于外部类实例,适用于逻辑上关联但不依赖于外部类实例的类。
什么是classpath?
classpath
是 Java
虚拟机(JVM)使用的一个环境变量,用来指示 JVM 如何搜索所需的
.class
文件(Java 字节码文件)。它定义了 JVM 在运行 Java
程序时应该去哪些地方寻找类文件,以便正确加载和执行。
classpath的作用
由于 Java 源码文件(.java
)在编译后生成
.class
文件,而 JVM 执行的是 .class
文件,因此
JVM 需要知道应该在哪里寻找这些文件。classpath
就是用来指定这些路径的。
如何设置classpath
classpath
可以通过两种方式设置:
在系统环境变量中设置(不推荐):这种方法会影响整个系统的所有 Java 程序,可能导致意外的问题。
在启动 JVM 时通过命令行设置(推荐):在启动 Java 程序时,通过传递
-classpath
或简写-cp
参数来设置classpath
,这是一种更灵活和安全的方式。示例:
这个命令指定了 JVM 应该依次在当前目录 (1
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
.
)、C:\work\project1\bin
、C:\shared
中查找abc.xyz.Hello
类。
classpath的默认值
如果没有显式设置 classpath
,JVM 的默认
classpath
是当前目录 (.
)。这意味着 JVM
会在当前目录下寻找类文件。
使用jar包
当项目中有很多 .class
文件时,将这些文件打包成一个
.jar
文件可以更方便地管理和分发。jar
文件本质上是一个压缩的 ZIP 文件,其中包含了目录结构和
.class
文件。我们可以通过将 .jar
文件包含在
classpath
中,让 JVM 在 .jar
文件中查找所需的类。
示例: 1
java -cp ./hello.jar abc.xyz.Hello
jar
包中包含了一个
META-INF/MANIFEST.MF
文件,并且这个文件指定了
Main-Class
,那么可以直接通过以下命令运行:
1
java -jar hello.jar
classpath
是 JVM 用来查找.class
文件的路径集合。- 推荐在启动 JVM 时使用
-cp
参数设置classpath
。jar
文件可以将多个.class
文件打包,以便更方便地管理和运行 Java 应用程序。
Java Class 文件版本问题
在 Java 开发中,不同版本的 JDK 会生成不同版本的 .class
文件,而这些 .class
文件只有在相应或更高版本的 JVM
上才能运行。
Class 文件版本
每个 JDK 版本对应特定的 .class
文件版本。例如:
- Java 8 对应
.class
文件版本 52 - Java 11 对应
.class
文件版本 55 - Java 17 对应
.class
文件版本 61
JVM 只能运行等于或低于其版本的 .class
文件。如果尝试在
Java 11 的 JVM 上运行 Java 17 编译的 .class
文件,会遇到
UnsupportedClassVersionError
错误。
兼容低版本 JVM
为了确保 .class
文件能在较低版本的 JVM
上运行,可以在编译时指定目标版本。例如,用 Java 17 编译 Java 11 兼容的
.class
文件:
1 | javac --release 11 Main.java |
这将确保编译出的 .class
文件兼容 Java 11 及更高版本的
JVM。
另一种方式是分别指定源码版本和目标 .class
版本:
1 | javac --source 9 --target 11 Main.java |
注意,--release
与
--source
、--target
选项是互斥的,不能同时使用。
潜在问题
如果使用高版本 JDK 编译低版本 .class
文件,可能会遇到不兼容的问题。例如,使用 Java 17 编译一个使用了 Java 12
引入的 String.indent()
方法的代码,即使目标是 Java 11 的
.class
文件,在 Java 11 上运行时仍会出现
NoSuchMethodError
。
因此,使用 --release
参数会更安全,因为它在编译时会检查源码是否使用了目标版本不支持的特性。
多版本 JDK 环境
在开发环境中,可以同时安装多个版本的 JDK,并通过设置
JAVA_HOME
环境变量来切换当前使用的 JDK 版本。
源码版本
编写源代码时,源码版本决定了可以使用的 Java 语法特性。例如:
- Lambda 表达式要求至少 Java 8 (
--source 8
) var
关键字要求至少 Java 10 (--source 10
)- Switch 表达式要求至少 Java 12 (
--source 12
)
有些新特性在发布前需要启用预览功能,例如 switch
表达式在
Java 12 和 13 版本中需要启用 --enable-preview
参数。
.class
文件版本与 JVM 版本紧密相关,使用javac
编译时可以指定目标版本。- 使用
--release
参数是确保编译输出兼容低版本 JVM 的最佳实践。- 多版本 JDK 环境下,可以通过
JAVA_HOME
切换 JDK 版本,确保源码与目标 JVM 版本兼容。
模块
模块化是Java 9引入的重要特性,它主要是为了解决“依赖管理”问题。简单来说,模块是比jar更高级的打包形式,它不仅包含class文件,还明确了依赖关系,允许模块之间清晰地相互引用。
什么是模块?
在Java
9之前,Java程序使用jar文件来打包class文件,但jar文件不关心class之间的依赖关系。这可能导致在运行时遇到ClassNotFoundException
的问题,因为某些依赖的jar文件可能被遗漏。模块引入后,除了将class文件打包外,还需定义模块的依赖关系。
模块的创建
模块的创建和传统的Java项目类似,只是在源代码目录中多了一个module-info.java
文件,这个文件定义了模块名和依赖关系。例如:
1 | module hello.world { |
在使用命令行工具时,我们可以使用javac
编译项目,并使用jar
命令生成jar包,接着使用jmod
将jar转换成模块文件:
1 | $ javac -d bin src/module-info.java src/com/itranswarp/sample/*.java |
模块的运行和JRE打包
模块可以通过java --module-path
命令运行,但.jmod
文件不能直接执行,需要通过jar
运行或用于打包JRE。
Java
9通过模块化将标准库的rt.jar
拆分成多个模块,使得只需打包运行时真正用到的部分,减少了JRE的体积。我们可以使用jlink
命令将应用和所需的模块打包为一个定制的JRE:
1 | $ jlink --module-path hello.jmod --add-modules java.base,java.xml,hello.world --output jre/ |
访问权限
模块还增强了访问权限的控制。默认情况下,模块内的class只能被同一模块访问,如果希望外部访问某个class,必须在module-info.java
中显式导出:
1 | module hello.world { |
这进一步提高了代码的封装性和安全性。
例子概述
假设我们要开发一个简单的Java程序,该程序有两个模块:
- greeting.module:用于提供问候功能。
- main.module:使用
greeting.module
中的功能并输出问候语。
步骤 1:创建模块结构
首先,我们在项目目录中创建以下结构:
1 | myapp/ |
步骤 2:编写模块代码
1. greeting.module 的代码
首先在greeting.module
中定义一个简单的问候类。
greeting.module/src/com/example/greeting/Greeting.java
:
1
2
3
4
5
6
7package com.example.greeting;
public class Greeting {
public String getGreetingMessage() {
return "Hello from Greeting Module!";
}
}
接着定义模块描述文件,声明该模块的导出包:
greeting.module/src/module-info.java
:
1
2
3module greeting.module {
exports com.example.greeting;
}
2. main.module 的代码
然后在main.module
中使用Greeting
类。
main.module/src/com/example/main/Main.java
:
1
2
3
4
5
6
7
8
9
10package com.example.main;
import com.example.greeting.Greeting;
public class Main {
public static void main(String[] args) {
Greeting greeting = new Greeting();
System.out.println(greeting.getGreetingMessage());
}
}
最后,定义main.module
的模块描述文件,声明对greeting.module
的依赖:
main.module/src/module-info.java
: 1
2
3module main.module {
requires greeting.module;
}
步骤 3:编译并运行模块
1. 编译模块
我们需要分别编译两个模块。假设当前工作目录是myapp
,可以执行以下命令:
编译greeting.module
:
1 | javac -d greeting.module/bin greeting.module/src/module-info.java greeting.module/src/com/example/greeting/Greeting.java |
编译main.module
:
1 | javac --module-path greeting.module/bin -d main.module/bin main.module/src/module-info.java main.module/src/com/example/main/Main.java |
2. 运行模块
编译完成后,可以通过以下命令运行main.module
:
1 | java --module-path greeting.module/bin:main.module/bin --module main.module/com.example.main.Main |
运行结果将输出:
1 | Hello from Greeting Module! |
解释
- 模块结构:
greeting.module
和main.module
是两个独立的模块,它们各自有自己的module-info.java
文件来定义模块名和依赖关系。 - 模块依赖:
main.module
通过requires greeting.module;
声明依赖greeting.module
,因此可以使用其中导出的Greeting
类。 - 访问控制:
greeting.module
中的Greeting
类被定义在导出的包中,因此可以被main.module
访问。