类与对象

面向对象语言

  • 封装性
  • 继承性
  • 多态性
  • 抽象性

可见性

在Java中,方法的访问修饰符控制着方法的可见性,即它们可以被哪些其他类或实例访问。除了访问修饰符之外,还有一些非访问修饰符,它们提供了其他类型的控制或信息。以下是这些修饰符的说明:

访问修饰符

  1. public

    • 任何其他类都可以访问 public 方法,不论它们是否在同一个包中。
  2. private

    • private 方法只能被它们所属的类内部访问。它们不能被其他类或子类访问。
  3. protected

    • protected 方法可以被同一个包内的其他类访问,也可以被不同包中的子类访问。
  4. 默认(无修饰符)

    • 如果没有指定访问修饰符,那么方法具有默认的包访问级别,即只能被同一个包内的其他类访问。

非访问修饰符

  1. static

    • static 方法属于类本身,而不是类的任何对象。这意味着你可以在没有创建类的对象的情况下调用 static 方法。
    • 它们通常用于工具方法,如数学计算,或用于初始化操作,如加载资源。
  2. final

    • final 方法不能被子类覆盖。这可以用来防止修改方法的行为,确保它在继承体系中保持不变。
  3. abstract(抽象的):

    • abstract 方法没有实现,它们只声明了方法的签名。包含 abstract 方法的类必须是抽象类,不能被实例化。
    • 抽象类和方法用于定义接口或行为的蓝图,具体的实现由子类提供。
  4. synchronized

    • synchronized 方法在多线程环境中用于防止多个线程同时执行该方法,确保线程安全。
  5. native

    • native 方法是使用Java以外的语言(通常是C或C++)实现的。它们提供了一种从Java代码调用非Java代码的方式。

示例

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
public class MyClass {

// 公有方法,可以被任何其他类访问
public void publicMethod() {
// ...
}

// 私有方法,只能被当前类访问
private void privateMethod() {
// ...
}

// 受保护的方法,可以被同一个包内的其他类或子类访问
protected void protectedMethod() {
// ...
}

// 默认访问级别方法,只能被同一个包内的其他类访问
void defaultMethod() {
// ...
}

// 静态方法,属于类而不是对象,可以在没有创建对象的情况下调用
public static void staticMethod() {
// ...
}

// 最终方法,不能被子类覆盖
public final void finalMethod() {
// ...
}

// 抽象方法,没有实现,类必须是抽象类
public abstract void abstractMethod();

// 同步方法,确保线程安全
public synchronized void synchronizedMethod() {
// ...
}

// 本地方法,用非Java语言实现
public native void nativeMethod();

// 一个方法可以同时使用多个修饰符
public static final void anotherMethod() {
// ...
}
}

在设计类和方法时,合理使用这些修饰符对于控制访问级别、确保线程安全、提供类的行为约束等都是非常重要的。

基本格式

1
2
3
4

class 类名{
类体的内容
}

类的声明

1
2
3
4
5
6
7
class People{
...
}

class 植物{
...
}

类体

1
2
3
4
5
6
7
8
9
10
11
12
13
class Ladder{
float above;
float bottom;
float height;
float area;
float computer(){
area=(above+bottom)*height/2.0f;
return area;
}
void setHeight(float h){
height=h;
}
}

成员变量

成员变量可以是Java中任何一种数据类型

  • 逻辑型(boolean:默认值是 false
  • 整数型(byte, short, int, long):所有整数类型的默认值是 0
  • 浮点型(float, double)float 类型的默认值是 0.0fdouble 类型的默认值是 0.0d
  • 字符型(char):默认值是空字符 \u0000,(教材写作NULL)即数值为0的字符。

这些默认值适用于类的成员变量(也称为字段或属性),不适用于局部变量。局部变量不会自动初始化为默认值,必须在使用前显式初始化。

成员变量的有效范围

在整个类的所有的方法里都有效,其有效与它在类体中出现的位置无关

上述Ladder类等效于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Ladder{
float above;
float bottom;

float area;
float computer(){
area=(above+bottom)*height/2.0f;
return area;
}
float height;
void setHeight(float h){
height=h;
}
}

方法

1
2
3
4
5
6
7
8
int speak() //无参
{
return 23;
}
int add(int x,int y,int z)//有参
{
return x+y+z;
}

注:当一个方法是void类型,该方法不需要返回值

方法体

方法体中声明的变量和方法的参数被称为局部变量

局部变量只在方法内有效

如果局部变量是在循环语句中,那么该局部变量的有效范围是在该循环语句内

区分局部变量和成员变量

如果局部变量和成员变量的名字相同,那么成员变量被隐藏,即该成员变量在这个方法内暂时失效(就近原则

1
2
3
4
5
6
7
class temple1 {
int x=10,y;
void f(){
int x=5;
y=x+x;//y=10
}
}

如果想在该方法中使用被隐藏的成员变量,必须使用关键字this指定 this.成员变量

1
2
3
4
5
6
7
class temple2 {
int x=10,y;
void f(){
int x=5;
y=x+this.x;//y=15
}
}

注:局部变量没有默认值

构造方法

构造方法在面向对象编程中扮演着至关重要的角色,因为它们确保了对象在使用前能够被正确地初始化。

构造方法的名字必须与所在的类的名字完全相同,而且没有类型

一个类中可以有多个构造方法但必须保证他们的参数不同(个数,类型不同)

构造方法的主要作用是初始化对象的状态,即设置对象在开始时应具有的值和状态。这是确保对象在被使用之前已经准备好其必要的数据和资源的关键步骤。

  1. 名称:构造方法的名称必须与定义它的类完全相同。
  2. 返回类型:构造方法没有返回类型,即使是void也不行。
  3. 重载:一个类可以有多个构造方法,只要它们的参数列表不同(不同的参数个数或类型)。
  4. 初始化:构造方法的主要目的是初始化对象的状态,设置初始值和资源。
  5. 默认构造方法:如果类中没有定义任何构造方法,编译器会提供一个无参的默认构造方法,其方法体为空。

构造方法的作用:

  • 为新对象分配内存。
  • 初始化对象的属性。
  • 执行任何需要的设置或资源分配。

自定义构造方法的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {
int x, y;

// 无参构造方法,默认初始化x和y为1
Point() {
this.x = 1;
this.y = 1;
}

// 带有两个参数的构造方法,用于指定x和y的值
Point(int a, int b) {
this.x = a;
this.y = b;
}
}

在这个例子中,Point 类提供了两种不同的构造方法:

  1. 无参构造方法:当使用 new Point() 创建对象时,会调用此构造方法,并将 xy 初始化为默认值1。
  2. 参数化构造方法:当使用 new Point(2, 3) 创建对象时,会调用此构造方法,并根据提供的参数初始化 xy

构造方法的调用:

  • 构造方法在创建类的新实例时自动调用。
  • 可以通过 new 关键字后跟构造方法名和相应的参数来调用构造方法。

注:如果没有编写构造方法,系统会默认该类只有一个构造方法,该默认构造方法是无参的,且方法体中没有语句。例:

1
2
Ladder(){
}

构造方法重载

构造方法重载允许一个类拥有多个同名但参数不同的构造方法,这样可以根据需要选择使用不同的构造方法来创建对象。这增加了类的灵活性,使得类的实例化过程可以更加多样和方便。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Student {
String name;
int age;
double gpa;

// 无参构造方法
public Student() {
this("Unknown", 0, 0.0);
}

// 带有两个参数的构造方法
public Student(String name, int age) {
this(name, age, 0.0);
}

// 带有三个参数的构造方法
public Student(String name, int age, double gpa) {
this.name = name;
this.age = age;
this.gpa = gpa;
}
}

在这个例子中,Student 类有三个构造方法,它们都叫做 Student,但参数列表不同。无参构造方法使用默认参数值来调用带有三个参数的构造方法。带有两个参数的构造方法同样调用带有三个参数的构造方法,但是为 gpa 提供了默认值。

当创建 Student 类的对象时,可以根据需要选择调用不同的构造方法:

1
2
3
Student student1 = new Student(); // 使用无参构造方法
Student student2 = new Student("Alice", 20); // 使用两个参数的构造方法
Student student3 = new Student("Bob", 22, 3.5); // 使用三个参数的构造方法

构造方法重载使得 Student 类可以以不同的方式初始化,而不必修改类的设计。这在实际编程中非常有用,尤其是在需要处理多种不同情况的类设计中。


对象

对象是面向对象编程(OOP)中的一个基本概念,代表现实世界中的实体或概念。在Java中,对象是通过类的实例化创建的,并且每个对象都拥有其自己的状态和行为。

对象的声明与创建

  • 声明:在Java中,对象的声明涉及指定对象的类型和名称。

    1
    类名 对象名;

    例如:

    1
    Ladder ladder;
  • 创建:使用 new 关键字后跟类的构造方法来创建对象。new 运算符负责分配内存并调用构造方法以初始化对象。

    1
    类名 对象名 = new 类名(参数列表);

    例如:

    1
    2
    Point p1 = new Point(10, 20);
    Point p2 = new Point(23, 25);

对象的内存分配

  • 栈与堆:在Java中,对象的内存分配涉及两个主要区域:栈和堆。
    • 栈(Stack):用于存储基本数据类型和对象引用(指针)。栈由JVM自动管理,存取速度快,但大小固定,不够灵活。
    • 堆(Heap):用于存储对象的实际数据。堆的大小可以动态变化,JVM负责垃圾回收,自动清理不再使用的对象。

创建多个对象

  • 一个类可以通过 new 运算符创建多个对象,每个对象都拥有独立的内存空间。
    1
    2
    Xiyoujirenwu zhubajie = new XiyoujiRenwu();
    Xiyoujirenwu sunwukong = new XiyoujiRenwu();

使用对象

  • 访问变量:通过点(.)操作符,可以访问对象的成员变量。

    1
    对象.变量;
  • 调用方法:同样使用点操作符来调用对象的成员方法。

    1
    对象.方法();

对象的生命周期

  • 对象从 new 表达式被执行时开始创建,直到没有任何引用指向它时结束。Java的垃圾收集器会定期检查并释放未被引用的对象所占用的内存。

示例

以下是如何使用对象的完整示例:

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
class Xiyoujirenwu {
float height, weight;
String head, ear;

void speak(String s) {
head = "歪着头";
System.out.println(s);
}
}

public class test1 {
public static void main(String[] args) {
Xiyoujirenwu zhubajie = new Xiyoujirenwu(); // 创建对象
zhubajie.height = 1.8f;
zhubajie.weight = 1.62f;
zhubajie.head = "大头";
zhubajie.ear = "一双大耳朵";

System.out.println("猪八戒的头为:" + zhubajie.head);
System.out.println("猪八戒的身高为:" + zhubajie.height);
System.out.println("猪八戒的体重为:" + zhubajie.weight);

zhubajie.speak("俺老朱想娶媳妇");

System.out.println("猪八戒的头为:" + zhubajie.head);
}
}

在这个示例中,Xiyoujirenwu 类的对象 zhubajie 被创建并初始化,然后通过点操作符访问其变量和方法。

对象的引用和实体

分配给对象的变量被习惯地称作对象的实体

1、避免使用空对象

2、重要结论

一个类声明的两个对象如果具有相同的引用,两者就具有完全相同的变量(实体)

在JAVA中,对于同一个类的两个对象object1和object2,允许进行如下操作:

1
object1=object2;

这样object2将引用指向object1,即object1,object2共同指向object1所指向变量。

3、垃圾收集

一个类声明的两个对象如果具有相同的引用,两者就具有完全相同的实体,Java有垃圾收集机制,这种机制周期性的检测某个实体是否不在被任何对象引用,如果发现这样的实体,就释放实体所占用的内存。

参数传值

参数属于局部变量。方法被调用时,参数变量必须有具体的值

基本数据类型参数的传值

参数传递的值的级别不可以高于该参数的级别;例如:不可以向int型参数传递一个float值,但是可以向double型参数传递一个float级别

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class Computer{
int add(int x,int y){
return x+y;
}
}

public class test1 {
public static void main(String []args){
Computer com =new Computer();
int m=100;int n=200;
int result =com.add(m,n);
System.out.println("m+n=:"+result);
}
}

引用类型参数的传值

Java的引用型数据包括数组,对象,接口

注:当参数是引用类型时,“传值”传递的是变量中存放的“引用”,而不是变量所引用的实体

可变参数

在声明方法时不给出参数列表中从某项开始直至最后一项参数的名字和个数,但这些参数的类型必须相同。可变参数使用“…”表示若干个参数

例如:

1
public void f(int ...x)

再如:

1
public void g(double a,int...x)

注:

1
public void method(int ...x,int y)//wrong

错误的,可变参数x即“参数代表”必须是参数列表中的最后一个。

子类与继承

继承

在Java中,继承是一种机制,允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。继承是面向对象编程中的一个核心概念,它支持代码复用和建立一个层次结构。

特点

  1. 代码复用:子类可以继承父类的属性和方法,无需重新编写代码。
  2. 层次结构:可以建立一个类层次结构,使得代码更加有组织。
  3. 可扩展性:通过继承,可以轻松地扩展现有类的功能。

访问修饰符与继承

  • public 类成员:可以被任何其他类访问,包括子类和非子类。
  • protected 类成员:可以被同一个包中的类访问,以及不同包中的子类访问。
  • private 类成员:只能被其所在的类访问,不能被子类访问。

继承的语法

1
2
3
4
5
6
7
public class ParentClass {
// 父类成员
}

public class ChildClass extends ParentClass {
// 子类成员
}

在这个例子中,ChildClass 继承了 ParentClass 的所有 publicprotected 类成员。

方法覆盖(Override)

子类可以覆盖父类的方法,即使用相同的方法名和参数列表,但提供不同的实现。

1
2
3
4
5
6
7
8
9
10
11
12
public class ParentClass {
public void display() {
System.out.println("Display method of ParentClass");
}
}

public class ChildClass extends ParentClass {
@Override
public void display() {
System.out.println("Display method of ChildClass");
}
}

在这个例子中,ChildClass 覆盖了 ParentClassdisplay 方法。

构造方法和继承

  • 子类无法继承父类的构造方法。
  • 子类的构造方法必须调用父类的构造方法,通常是通过 super() 调用。
1
2
3
4
5
6
7
8
9
10
11
12
public class ParentClass {
public ParentClass() {
// 父类的构造方法
}
}

public class ChildClass extends ParentClass {
public ChildClass() {
super(); // 调用父类的构造方法
// 子类的构造方法
}
}

继承的注意事项

  • 避免过度使用继承,因为过度的继承可能导致代码难以理解和维护。
  • 优先使用组合而不是继承,除非确实需要表示一个“是一个”(is-a)的关系。

示例

假设我们有一个 Animal类,它有一些基本属性和方法,然后我们有两个子类 DogCat,它们继承了 Animal类并添加了特定的行为。

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
class Animal {
protected String name;

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

public void eat() {
System.out.println(name + " is eating.");
}
}

class Dog extends Animal {
public Dog(String name) {
super(name); // 调用父类的构造方法
}

public void bark() {
System.out.println(name + " barks.");
}
}

class Cat extends Animal {
public Cat(String name) {
super(name); // 调用父类的构造方法
}

public void meow() {
System.out.println(name + " meows.");
}
}

public class TestInheritance {
public static void main(String[] args) {
Animal myAnimal = new Animal("Generic Animal");
Dog myDog = new Dog("Buddy");
Cat myCat = new Cat("Whiskers");

myAnimal.eat();
myDog.eat();
myDog.bark();
myCat.eat();
myCat.meow();
}
}

在这个例子中,DogCat都继承了 Animal类,并能够使用 eat方法,同时也有它们自己的特定行为 barkmeow。这展示了继承如何帮助我们创建一个层次结构,同时保持代码的复用性和扩展性。

多态

多态是指允许不同类的对象对同一消息做出响应的能力,但具体的行为会根据对象的实际类型来确定。多态性使得代码可以对数据类型进行一般化处理,而不需要关心具体的类。

多态性有两种主要形式:

  1. 编译时多态(静态多态/方法重载):通过方法重载实现,即在同一个类中可以有多个同名方法,只要它们的参数列表不同(参数的个数或类型不同)。
  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
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}

class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}

public class TestPolymorphism {
public static void main(String[] args) {
Animal myAnimal = new Animal();
Animal myDog = new Dog(); // 向上转型

myAnimal.makeSound(); // 输出: Animal makes a sound
myDog.makeSound(); // 输出: Dog barks

// 多态的使用:通过父类引用调用子类对象的方法
Animal animal = new Dog();
animal.makeSound(); // 输出: Dog barks
}
}

在这个例子中,Dog 类继承了 Animal 类并重写了 makeSound 方法。当我们通过 Animal 类型的引用调用 makeSound 方法时,实际调用的是 Dog 类的版本,这就是运行时多态的体现。

super

super关键字在Java中有几个用途,但主要是用来引用直接父类的信息。以下是 super的一些常见用法:

  1. 调用父类的构造方法
    子类的构造方法可以通过 super()来调用直接父类的构造方法。这通常是在子类构造方法的第一行进行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Parent {
    public Parent() {
    // 初始化父类
    }
    }

    public class Child extends Parent {
    public Child() {
    super(); // 调用父类的构造方法
    // 初始化子类
    }
    }
  2. 访问父类的成员
    当子类覆盖了父类的属性或方法时,可以使用 super来访问父类中被覆盖的成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Parent {
    protected int value = 10;
    }

    public class Child extends Parent {
    private int value = 20;

    public int getValue() {
    return super.value; // 访问父类的value
    }
    }
  3. 调用父类的方法
    如果子类重写了父类的方法,可以使用 super来调用父类中原始的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Parent {
    public void show() {
    System.out.println("Parent's show()");
    }
    }

    public class Child extends Parent {
    @Override
    public void show() {
    super.show(); // 调用父类的show()
    System.out.println("Child's show()");
    }
    }

final

final关键字在Java中用于表示某个实体不可被修改或覆盖。以下是 final的一些用途:

  1. 最终变量
    被声明为 final的变量一旦被初始化后,其值就不能被改变。

    1
    2
    final int CONSTANT = 10;
    // CONSTANT = 20; // 错误:不能为final变量赋值
  2. 最终方法
    声明为 final的方法不能被子类覆盖。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Parent {
    public final void show() {
    // 方法体
    }
    }

    public class Child extends Parent {
    // @Override
    // public void show() { // 错误:无法覆盖final方法
    // }
    }
  3. 最终类
    声明为 final的类不能被继承,即没有其他类可以是它的子类。

    1
    2
    3
    4
    5
    public final class UtilityClass {
    // 类体
    }
    // public class SubClass extends UtilityClass { // 错误:无法继承final类
    // }
  4. 最终实例化
    使用 final关键字可以阻止对象被重新赋值给另一个新对象。

    1
    2
    3
    4
    public class MyClass {
    public static final MyClass INSTANCE = new MyClass();
    // private MyClass() {} // 推荐:使构造方法私有,防止外部创建新实例
    }

superfinal在Java中扮演着重要的角色,super允许子类与父类交互,而 final提供了一种机制来保证代码的稳定性和不可变性。

上转型的特点:

在Java中,上转型(Upcasting)是指将子类的对象赋值给父类引用的过程。由于Java支持单继承,这意味着每个类只有一个直接父类,所以向上转型是安全的,因为子类是父类的一个“特殊版本”。上转型是隐式进行的,不需要进行任何显式转换。

  1. 隐式转换:Java编译器会自动将子类引用转换为父类引用。
  2. 类型安全:由于子类继承了父类的所有属性和方法,所以上转型不会违反类型安全。
  3. 无需强制类型转换:与下转型(Downcasting)不同,上转型不需要强制类型转换,因为它是安全的。

上转型的例子:

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
class Animal {
public void eat() {
System.out.println("Animal eats food");
}
}

class Dog extends Animal {
public void bark() {
System.out.println("Dog barks");
}
}

public class TestUpcasting {
public static void main(String[] args) {
Dog myDog = new Dog(); // 创建Dog对象
Animal myAnimal = myDog; // 上转型:隐式转换Dog到Animal

// 调用Animal类的方法eat
myAnimal.eat();

// 由于myAnimal是Animal类型,我们不能直接调用Dog类的bark方法
// myAnimal.bark(); // 错误:Animal类中没有bark方法

// 但是我们可以重新将Animal引用转换回Dog引用来调用bark方法
if (myAnimal instanceof Dog) {
((Dog) myAnimal).bark();
}
}
}

在这个例子中,Dog 类是 Animal 类的子类。我们创建了一个 Dog 类的对象 myDog,然后将其赋值给 Animal 类型的引用 myAnimal。这个过程就是上转型。由于 myAnimalAnimal 类型,我们只能调用 Animal 类中定义的方法,而不能直接调用 Dog 类特有的 bark 方法。

上转型的应用:

  • 多态性:上转型是实现多态性的关键。通过将子类对象赋值给父类引用,可以利用多态性调用子类重写的方法。
  • 通用编程:在编写可以接受多种子类对象的方法时,通常使用父类类型作为参数或局部变量,这样可以处理所有子类的对象。
  • 接口实现:当多个类实现同一个接口时,接口类型的引用可以指向任何一个实现类的实例,这也是上转型的一个应用。

抽象类

在Java中,abstract关键字用于定义抽象类和抽象方法。

抽象类是一种不能被实例化的类,它通常作为其他类的基类使用。抽象类可以包含抽象方法和具体方法。

抽象类的特点

  1. 不能实例化:你不能创建抽象类的实例。
  2. 包含抽象方法:抽象类可以包含一个或多个抽象方法。
  3. 可以包含具体方法:抽象类也可以包含具有实现的非抽象方法。
  4. 可以有成员变量:抽象类可以有成员变量,这些变量可以是具体类型或抽象类型。
  5. 子类必须实现抽象方法:如果子类是非抽象的,那么它必须实现其抽象父类中的所有抽象方法。

定义抽象类的语法

1
2
3
abstract class AbstractClass {
// 抽象方法和具体方法
}

抽象方法

抽象方法是没有实现体的方法,它只声明了方法的签名和返回类型,但没有方法体。

抽象方法的特点

  1. 没有方法体:抽象方法只有方法的声明,没有方法体。
  2. 使用 abstract关键字:在方法声明前使用 abstract关键字。
  3. 不能在非抽象类中声明:抽象方法必须在抽象类中声明。
  4. 子类必须提供实现:如果子类是非抽象的,那么它必须为所有继承的抽象方法提供实现。

定义抽象方法的语法

1
2
3
abstract class AbstractClass {
abstract returnType methodName(parameters);
}

示例

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
abstract class Animal {
// 抽象方法,没有方法体
abstract void makeSound();

// 具体方法,有方法体
void eat() {
System.out.println("Animal eats food");
}
}

class Dog extends Animal {
// 实现抽象方法
@Override
void makeSound() {
System.out.println("Dog barks");
}
}

public class TestAbstract {
public static void main(String[] args) {
// 错误:不能创建抽象类的实例
// Animal myAnimal = new Animal();

Dog myDog = new Dog(); // 正确的:Dog是非抽象子类
myDog.makeSound(); // 输出: Dog barks
myDog.eat(); // 输出: Animal eats food
}
}

在这个示例中,Animal 是一个抽象类,它包含一个抽象方法 makeSound() 和一个具体方法 eat()Dog 类继承了 Animal 类并实现了 makeSound() 方法。main 方法中,我们不能创建 Animal 类的实例,但可以创建 Dog 类的实例,并调用它的方法。

抽象类和抽象方法的作用

抽象类和抽象方法的主要作用是为其他类提供一个模板或蓝图。它们允许你定义一个接口或一组方法,然后让子类来提供具体的实现。这种设计模式有助于创建一个灵活的、可扩展的类层次结构,使得代码更加模块化和易于维护。

接口

在Java中,接口(Interface)是一种引用类型,它定义了一组方法规范,这些方法需要被实现接口的类具体实现。接口是Java实现完全抽象和多态的重要机制之一。

  1. 完全抽象:接口中的方法默认都是抽象的,没有方法体。
  2. 多实现:一个类可以实现多个接口,但只能继承自一个类。
  3. 默认方法:从Java 8开始,接口可以包含具有默认实现的方法,这些方法可以有方法体。
  4. 静态方法:接口也可以包含静态方法,这些方法同样可以有方法体。
  5. 私有方法:Java 9开始,接口中可以包含私有方法。
  6. 常量:接口中可以定义常量,通常以全大写字母表示。
  7. 不允许实例化:接口不能被直接实例化,但可以通过实现接口的类的实例来访问接口中定义的方法。

接口的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface MyInterface {
// 抽象方法
void myMethod();

// 默认方法
default void myDefaultMethod() {
// 方法实现
}

// 静态方法
static void myStaticMethod() {
// 方法实现
}
}

实现接口:

要实现接口,类需要使用 implements关键字,并提供接口中所有抽象方法的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass implements MyInterface {
// 实现接口中的抽象方法
public void myMethod() {
// 方法实现
}

// 可以选择性地覆盖接口中的默认方法
@Override
public void myDefaultMethod() {
// 覆盖默认实现
}
}

接口的用途:

  1. 定义规范:接口定义了一组规范,任何实现该接口的类都必须遵循这些规范。
  2. 多态:接口允许不同的对象对同一消息做出响应,实现多态性。
  3. 解耦:接口降低了系统各部分之间的耦合度,提高了系统的灵活性和可扩展性。
  4. 组合:接口可以被多个类实现,从而实现功能上的组合。

接口与抽象类的区别:

  • 抽象类可以有具体的方法实现,而接口中的方法默认都是抽象的(Java 8之前)。
  • 抽象类可以有成员变量,而接口中只能有常量(Java 8之前)。
  • 抽象类只能继承自一个类,而一个类可以实现多个接口
  • 抽象类更倾向于“是一个”(is-a)的关系,而接口更倾向于“能做”(can-do)的约定。

接口的示例:

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
public interface Vehicle {
void start();
void stop();
}

public class Car implements Vehicle {
public void start() {
System.out.println("Car starts");
}

public void stop() {
System.out.println("Car stops");
}
}

public class Bicycle implements Vehicle {
public void start() {
System.out.println("Bicycle pedals start");
}

public void stop() {
System.out.println("Bicycle stops");
}
}

public class TestInterfaces {
public static void main(String[] args) {
Vehicle myCar = new Car();
myCar.start();
myCar.stop();

Vehicle myBike = new Bicycle();
myBike.start();
myBike.stop();
}
}

在这个示例中,Vehicle 是一个接口,定义了 start()stop()两个方法。CarBicycle类实现了 Vehicle接口,并提供了这两个方法的具体实现。TestInterfaces类中的 main方法展示了如何通过接口引用来使用实现类的实例,这体现了多态性。

接口是Java中实现多态和定义规范的强大工具,它们使得代码更加灵活和可维护。

接口回调:

接口回调是一种设计模式,它允许一个对象将某种操作委托给另一个对象,在某些情况下,还可以指定回调函数。在Java中,接口回调通常通过定义一个接口并在其他类中实现该接口来实现。这种方式使得代码更加灵活和可扩展。

  1. 定义回调接口:首先,定义一个接口,该接口声明了将要被回调的方法。
  2. 实现回调接口:然后,创建一个或多个类来实现这个接口,提供接口中声明方法的具体实现。
  3. 注册回调:在需要使用回调的类中,提供一个方法或构造函数,允许客户端代码传递一个实现了回调接口的对象。
  4. 触发回调:在适当的时候,使用客户端传递的回调对象来调用其方法,从而实现回调。

接口回调的示例:

假设我们有一个 Button类,它有一个点击事件。我们希望当按钮被点击时,能够触发一个回调操作。

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
// 定义回调接口
interface ButtonClickListener {
void onClick();
}

// 实现回调接口
class MyButtonClickListener implements ButtonClickListener {
public void onClick() {
System.out.println("Button was clicked!");
}
}

// 按钮类,包含回调逻辑
class Button {
private ButtonClickListener clickListener;

// 注册回调
public void setOnClickListener(ButtonClickListener listener) {
this.clickListener = listener;
}

// 模拟按钮点击
public void click() {
if (clickListener != null) {
clickListener.onClick(); // 触发回调
}
}
}

// 测试类
public class TestButton {
public static void main(String[] args) {
Button myButton = new Button();
MyButtonClickListener myListener = new MyButtonClickListener();

// 注册回调
myButton.setOnClickListener(myListener);

// 模拟点击按钮
myButton.click(); // 输出: Button was clicked!
}
}

在这个例子中,ButtonClickListener是一个接口,它定义了一个 onClick方法,这个方法将在按钮被点击时被调用。MyButtonClickListener类实现了 ButtonClickListener接口,并提供了 onClick方法的具体实现。Button类包含一个 setOnClickListener方法,允许客户端代码注册一个 ButtonClickListener对象。当 Buttonclick方法被调用时,它会检查是否有注册的监听器,并调用其 onClick方法,从而触发回调。

接口回调的应用场景:

  • 事件处理:如GUI编程中的事件监听器。
  • 异步编程:在异步操作完成时回调一个方法。
  • 回调函数:在某些API中,允许用户传递一个回调函数来处理特定事件。

接口回调是一种强大的模式,它提供了一种松耦合的方式来处理事件和操作,使得代码更加模块化和易于维护。在现代编程中,接口回调被广泛应用于各种场景,包括事件驱动编程、异步编程等。