Java八股(面向对象)

什么是面向对象?

面向对象是一种编程范式,它将现实世界中的事物抽象为对象,对象具有属性(称为字段或属性)和行为(称为方法)。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的功能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。

什么是封装、继承、多态(Java三大特性)

封装:一个标准的JavaBean,能够把对象的属性和方法结合在一起,对外隐藏对象的具体细节,只通过对象对外提供的接口进行交互。保证了安全性和降低了编程难度,降低了代码之间的耦合。

继承:是一种让字类可以自动共享父类的数据结构与方法的机制,可以提高代码的服用率。通过继承可以建立类之间的层级关系。

多态:容许使用同样的方法名去调用,具体的实现根据对象的不同而不同。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal {
public void speak() {
System.out.println("Animal speak");
}
}

class Dog extends Animal {
@Override
public void speak() {
System.out.println("Woof");
}
}

class Cat extends Animal {
@Override
public void speak() {
System.out.println("Meow");
}
}
1
2
3
4
5
Animal a1 = new Dog();
Animal a2 = new Cat();

a1.speak(); // 调用的其实是 Dog 的实现 → 输出 Woof
a2.speak(); // 调用的是 Cat 的实现 → 输出 Meow

对于同一个接口,或者同一个父类,更具其具体实现的子类方法,可以实现不同的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface PayService {
void pay(double amount);
}

class WechatPay implements PayService {
public void pay(double amount) {
System.out.println("用微信支付:" + amount);
}
}

class Alipay implements PayService {
public void pay(double amount) {
System.out.println("用支付宝支付:" + amount);
}
}
1
2
3
4
5
6
void doPay(PayService payService) {
payService.pay(100);
}

doPay(new WechatPay()); // 微信逻辑
doPay(new Alipay()); // 支付宝逻辑

根据多态的发生时机,可以分为编译时多态运行时多态。

编译时多态(方法的重载)

Point:在编译阶段就能决定调用哪个方法。不是严格的多态。
编译器看到你的参数类型,就已经选好了具体哪个重载版本,跟运行时对象没关系,所以叫“编译时多态”。

1
2
3
4
5
void print(int x) { System.out.println("int"); }
void print(String s) { System.out.println("String"); }

print(1); // 编译期就决定:走 print(int)
print("abc"); // 编译期就决定:走 print(String)

运行时多态(方法的重写)

方法的重载+父类引用指向子类对象

Point:编译期只能看到“父类/接口类型”,真正执行哪个实现,要到运行时看“真实对象类型”

1
2
3
Animal a = new Dog();
a.speak(); // 编译期只知道 a 是 Animal 类型
// 运行期看 a 里实际装的是 Dog,最终调用 Dog.speak()

这个“运行时才决定具体调用哪个版本”的特性,就是“运行时多态”。

多态的价值

能够降低代码的耦合,方便维护和拓展。

假设目前的需求是写出“支付宝”的支付方式,但是现在接入了微信的支付方式。那么如果不用多态,就得在程序中写多个If else进行判断当前是哪种支付方式。这样增加代码进行拓展时候的风险。

如果使用多态,建立多个PayService接口的实现类,那么就能够只使用一个支付方法,将具体的对象参数传入支付方法,编译器和虚拟机会根据具体的对象种类决定调用哪个实例中的方法。

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
public interface PayService{
void pay(int n){}
}

public class Paypal implements PayService {
@Override
public void pay(int n){
System.out.println("使用支付宝支付"+ n +"元。");
}
}

public class WechatPay implements PayService {
@Override
public void pay(int n){
System.out.println("使用微信支付"+ n +"元。");
}
}

//输出:
PayService pay1 = new Paypal();
PayService pay2 = new WechatPay();

pay1.pay(100);//会在控制台输出:使用支付宝支付100元。
pay2.pay(200);//使用微信支付200元。

void checkout(PayService payService) {
payService.pay(100);
}

checkout(pay1);//给checkou方法传一个payService类型的对象就能实现用支付宝支付

//不用进行条件语句的判断也能知道使用什么方法。

接口的标准写法

Java 8 前的标准写法:接口关键词,常量,抽象方法

1
2
3
4
5
6
7
8
9
10
public interface PayService {

// 1. 常量(可选):默认就是 public static final
int TIMEOUT_SECONDS = 30;

// 2. 抽象方法:默认就是 public abstract,可以省略
void pay(int amount);

String queryOrder(String orderId);
}

Java 8+ 之后的标准写法:default 方法,静态方法(可以通过 “接口.方法名” 直接调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface XxxService {

// 常量(可选)
int SOME_CONSTANT = 1;

// 抽象方法(必须实现)
ReturnType methodName(ParamType param);

// 默认方法(可选)
default void defaultMethod() {
// 默认实现
}

// 静态方法(可选)
static void utilMethod() {
// 工具逻辑
}
}

注意:静态方法不能被实现类重写,是写给“这个接口本身”用的工具方法
默认方法,可以被重写,但是也可以直接用,可以避免一些代码编写的时候的冗余

重载和重写方法的区别

接口方法在实现类中涉及到方法的 重写 标签:@Override。

就想到在子类继承父类的时候可以 重载 方法,于是在思考二者之间的区别。

重载(overload)≠ 重写(override)

实现接口 / 继承抽象类时,要求的是“重写抽象方法”,而不是“随便写个重载方法糊弄一下”

“重写(override)”必须保持:方法名 + 参数列表(个数、类型、顺序)完全一致。

想重写(Override)

名字一样 ➕ 参数列表一模一样 ➕ 返回值兼容 ➕ 访问权限不变或放大

想重载(Overload)

名字一样 ➕ 参数列表不一样(个数 / 类型 / 顺序不同)

多态作用域

方法重载

  • 方法重载是指同一类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同)。虽然方法名相同,但根据传入的参数不同,编译器会在编译时确定调用哪个方法。
  • 示例:对于一个 add 方法,可以定义为 add(int a, int b) 和 add(double a, double b)

方法重写

  • 方法重写是指子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的实际类型确定调用哪个版本的方法。这是实现多态的主要方式。
  • 示例:在一个动物类中,定义一个 sound 方法,子类 Dog 可以重写该方法以实现打印 bark,而 Cat 可以实现打印 meow

接口的实现

  • 多态也体现在接口的使用上,多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法。这使得程序在面对不同具体实现时保持一贯的调用方式。
  • 示例:多个类(如 DogCat)都实现了一个 Animal 接口,当用 Animal 类型的引用来调用 makeSound 方法时,会触发对应的实现。

对象向上/向下转型

  • 在Java中,可以使用父类类型的引用指向子类对象,这是向上转型。通过这种方式,可以在运行时期采用不同的子类实现。
  • 向下转型是将父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免 ClassCastException

面向对象的设计原则

面向对象编程中的六大原则:

  • 单一职责原则(SRP):一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。例子:考虑一个员工类,它应该只负责管理员工信息,而不应负责其他无关工作。
  • 开放封闭原则(OCP):软件实体应该对扩展开放,对修改封闭。例子:通过制定接口来实现这一原则,比如定义一个图形类,然后让不同类型的图形继承这个类,而不需要修改图形类本身。
  • 里氏替换原则(LSP):子类对象应该能够替换掉所有父类对象。例子:一个正方形是一个矩形,但如果修改一个矩形的高度和宽度时,正方形的行为应该如何改变就是一个违反里氏替换原则的例子。
  • 接口隔离原则(ISP):客户端不应该依赖那些它不需要的接口,即接口应该小而专。例子:通过接口抽象层来实现底层和高层模块之间的解耦,比如使用依赖注入。
  • 依赖倒置原则(DIP):高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。例子:如果一个公司类包含部门类,应该考虑使用合成/聚合关系,而不是将公司类继承自部门类。
  • **最少知识原则 (Law of Demeter)**:一个对象应当对其他对象有最少的了解,只与其直接的朋友交互。

抽象类的特殊性

  • 抽象类不能被实例化,只能被继承。
  • 抽象类中的方法可以有实现也可以没有实现。abstract关键词。
  • 一个类只能继承一个抽象类,实现多个接口。
  • 抽象类一般用于作为基类,被其他类继承和扩展。一般不直接使用抽象类。

注:抽象类不可以被直接new,但是可以当做继承了它的子类对象的类型。

1
AbstractAnimal a = new Dog();

“不能 new 抽象类” ≠ “不能用抽象类当类型”。

对象是new出来的那个类,但是这个对象可以被提升类型为“抽象类型”。

抽象类为什么作为“对象引用的类型”

1
2
3
4
5
6
7
8
9
10
public abstract class Animal {
public abstract void speak();
}

public class Dog extends Animal {
@Override
public void speak() {
System.out.println("汪汪汪");
}
}

我们现在有一个抽象父类Animal 和他的子类Dog。

那么下面的语句是合法的:

1
2
Animal a = new Dog();  // ✅
a.speak(); // 输出:汪汪汪
  • 抽象类 没有被实例化,被 new 的是子类 Dog

  • 抽象类 只是作为“引用的类型”存在:告诉编译器

    “这东西至少是一个 Animal,肯定有 speak() 这个方法。”


用抽象类做引用类型,好处在于:

  1. 可以实现多态:方法里面只写抽象父类名, 传递参数的时候可以直接new子类实现,自动提升类型为父类类型,这样就可以使用各自子类中的方法,同时还保持了方法的唯一性。
  2. 解耦+易拓展:

假设现在有抽象类:

1
2
3
4
5
6
7
8
9
public abstract class PayTemplate {
public void pay(int amount) {
checkBalance();
doPay(amount); // 抽象步骤
printReceipt(amount);
}

protected abstract void doPay(int amount); // 抽象方法
}

有该抽象类的子类,但是只重写了doPay()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Alipay extends PayTemplate {
@Override
protected void doPay(int amount) {
System.out.println("支付宝扣款:" + amount);
}
}

public class WechatPay extends PayTemplate {
@Override
protected void doPay(int amount) {
System.out.println("微信扣款:" + amount);
}
}

那么这样就实现了解耦,支付的pay模版过程不用改,只是支付方式通过各自子类重写的doPay()方法执行。

调用时:

1
2
PayTemplate pay = new Alipay();   // 或 new WechatPay()
pay.pay(100);

**写逻辑的时候,变量类型是抽象类 PayTemplate**,完全不依赖某个具体实现
以后要加其他的支付手段的时候,再新建PayTemplate 的子类,并重写他们各自的doPay()方法即可。

抽象类和接口的区别

抽象类:

  • 继承抽象类的关键字为extends
  • 抽象类可以有定义与实现,方法可在抽象类中实现
  • 抽象类不能被实例化,只能被继承
  • 抽象类中的方法可以有实现也可以没有实现
  • 一个类只能继承一个抽象类,但可以同时实现多个接口
  • 抽象类一般用于作为基类,被其他类继承和扩展使用
  • 抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值
  • 抽象类可以包含实例变量和静态变量

接口:

  • 实现接口的关键字为implements
  • 接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8 以后可以有默认方法和静态方法)
  • 接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体
  • 接口成员变量默认为public static final,必须赋初值,不能被修改
  • 接口只能包含常量(即静态常量)

Java中的静态变量和静态方法

static关键词修饰的变量和方法,和类的实例无关系和类的本身有关系。类被加载后,静态方法和静态变量就被在内存中创建,并且只存在一份,可以被该类的所有实例去共享。

静态变量特点:

  • 共享性:所有该类的实例共享同一个静态变量。如果一个实例修改了静态变量的值,其他实例也会看到这个更改。
  • 初始化:静态变量在类被加载时初始化,只会对其进行一次分配内存。
  • 访问方式:静态变量可以直接通过类名访问,也可以通过实例访问,但推荐使用类名。

静态方法特点:

  • 无实例依赖:静态方法可以在没有创建类实例的情况下调用。也推荐使用类名直接调用
  • 访问静态成员:静态方法可以直接调用其他静态变量和静态方法,但不能直接访问非静态成员。
  • 多态性:静态方法不支持重写(Override),但可以被隐藏(Hide)。

内部类

静态内部类

静态内部类可以使用 static 关键字定义,静态内部类我们不需要创建外部类来访问,可以直接访问它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OuterClass {
int x = 10;

static class InnerClass {
int y = 5;
}
}

public class MyMainClass {
public static void main(String[] args) {
OuterClass.InnerClass myInner = new OuterClass.InnerClass();
System.out.println(myInner.y);
}
}

非静态内部类

非静态内部类是一个类中嵌套着另外一个类。 它有访问外部类成员的权限, 通常被称为内部类。

由于内部类嵌套在外部类中,因此必须首先实例化外部类,然后创建内部类的对象来实现。

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

class InnerClass {
int y = 5;
}
}

public class MyMainClass {
public static void main(String[] args) {
OuterClass myOuter = new OuterClass(); //先创建外部类的对象
OuterClass.InnerClass myInner = myOuter.new InnerClass();
System.out.println(myInner.y + myOuter.x);
}
}

内部类的高级用法

可以使用内部类去访问外部类的属性方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class OuterClass {
int x = 10;

class InnerClass {
public int myInnerMethod() {
return x;
}
}
}

public class MyMainClass {
public static void main(String[] args) {
OuterClass myOuter = new OuterClass();
OuterClass.InnerClass myInner = myOuter.new InnerClass();
System.out.println(myInner.myInnerMethod());
}
}

非静态内部类和静态内部类的区别

  • 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
  • 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
  • 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
  • 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化
  • 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。

非静态内部类如何访问到外部类的方法的

编译器在生成字节码时会为非静态内部类维护一个指向外部类实例的引用。

编译器会在生成非静态内部类的构造方法时,将外部类实例作为参数传入,并在内部类的实例化过程中建立外部类实例与内部类实例之间的联系,从而实现直接访问外部方法的功能。