一、怎样理解面向对象?简单说明封装,继承,多态
核心理解:面向对象(OOP)是一种编程思想,核心是"万物皆对象"------把现实中的事物抽象成程序里的"对象",每个对象都有自己的"属性"(比如人的年龄、姓名)和"行为"(比如人会吃饭、走路),通过对象之间的交互完成程序功能,比面向过程更灵活、易维护。
三大核心特性:
-
封装:把对象的属性和行为"包裹"起来,隐藏内部细节,只对外提供可访问的接口(比如手机,我们只需要按按键、触屏操作,不需要知道内部芯片、电路的工作原理),核心是"隐藏细节、暴露接口",比如Java中用private修饰属性,提供get/set方法访问。
-
继承:子类继承父类的属性和方法,减少重复代码,实现代码复用(比如狗和猫都继承"动物"类,动物有的"吃饭、睡觉"方法,狗和猫不用再重复写),Java中只有单继承(一个子类只能有一个父类),但可以多实现。
-
多态:同一个行为,作用在不同对象上,产生不同的结果(比如"叫声"这个行为,狗叫是"汪汪",猫叫是"喵喵"),核心是"父类引用指向子类对象",实现程序的灵活性和扩展性。
二、多态体现在哪几个方面?
Java中多态主要体现在3个方面
-
方法重写(最核心):子类重写父类的方法,父类引用指向子类对象时,调用的是子类重写后的方法(比如父类Animal有call()方法,子类Dog重写call()为"汪汪",Cat重写为"喵喵",Animal a = new Dog(); a.call() 执行Dog的call())。
-
方法重载:同一个类中,有多个方法名相同,但参数列表(参数个数、类型、顺序)不同的方法,调用时根据参数自动匹配(比如Math.abs(),可以接收int、double等不同类型参数,执行不同的逻辑)。
-
接口多实现:一个类可以实现多个接口,不同接口有相同的方法名,子类实现后,调用该方法时体现多态(比如接口A和接口B都有show()方法,类C实现A和B,重写show(),调用时执行C的show())。
补充:多态的前提是"继承/实现"、"方法重写"、"父类/接口引用指向子类对象",三者缺一不可。
三、多态解决了什么问题?
-
减少代码冗余,提高代码复用性:不用为每个子类单独写调用逻辑,统一用父类引用调用,比如遍历不同的动物,不用分别写Dog.call()、Cat.call(),直接用Animal引用调用即可。
-
提高程序的扩展性和灵活性:新增子类时,不用修改原有父类和调用逻辑,直接新增子类并重写方法即可,符合"开闭原则"(对扩展开放,对修改关闭),比如新增"猪"类,继承Animal,重写call(),原有代码无需改动就能调用。
简单说:多态让程序"写一次,适配多种场景",后期维护更省心。
四、面向对象的设计原则你知道有哪些吗?
-
单一职责原则:一个类只做一件事(比如User类只处理用户相关逻辑,不处理订单逻辑),目的是降低耦合,便于维护。
-
开闭原则(核心):对扩展开放,对修改关闭(新增功能靠新增类/方法,不修改原有代码),比如多态就体现了开闭原则。
-
里氏替换原则:子类可以完全替代父类,且不影响程序的正常运行(子类不能破坏父类的原有逻辑),比如Dog可以替代Animal,调用Animal的方法时,用Dog对象也能正常执行。
-
依赖倒置原则:依赖抽象(接口/抽象类),不依赖具体实现(子类),比如调用方法时,参数用接口类型,而不是具体的子类类型,提高灵活性。
-
接口隔离原则:一个接口只定义一个单一功能,不定义无关的方法(比如UserService接口只定义用户相关方法,不定义商品相关方法),避免接口臃肿。
-
迪米特法则(最少知道原则):一个类尽量少了解其他类的内部细节,只和直接关联的类交互(比如A类调用B类的方法,不用知道B类内部怎么实现的),降低耦合。
-
合成复用原则:优先用"组合/聚合"的方式复用代码,而不是继承(比如Car类可以包含Engine类,复用Engine的功能,而不是继承Engine),避免继承带来的耦合过高。
五、重载与重写有什么区别?
| 对比维度 | 重载(Overload) | 重写(Override) |
|---|---|---|
| 定义 | 同一个类中,方法名相同,参数列表不同 | 子类与父类中,方法名、参数列表、返回值(兼容)完全相同 |
| 范围 | 同一个类 | 子类与父类(或接口与实现类) |
| 修饰符 | 无限制(可任意修饰) | 子类修饰符不能严于父类(比如父类是public,子类不能是private) |
| 核心目的 | 方便调用,同一功能适配不同参数 | 重写父类逻辑,实现子类特有功能 |
| 示例 | add(int a)、add(int a, int b) | 父类Animal.call(),子类Dog.call() |
补充:重载不看返回值类型(哪怕返回值不同,参数列表相同也不算重载);重写必须看返回值类型(要和父类兼容,比如父类返回int,子类不能返回String)。
六、抽象类和普通类有什么区别?
核心区别:抽象类"不完整",不能直接实例化;普通类"完整",可以直接实例化,具体区别如下:
-
实例化能力:普通类可以直接用new关键字实例化(比如new User());抽象类不能直接实例化(用abstract修饰,new AbstractClass()会报错),只能被子类继承后,实例化子类。
-
方法定义:普通类只能定义具体方法(有方法体,能直接执行);抽象类可以定义抽象方法(用abstract修饰,没有方法体,必须由子类重写),也可以定义具体方法。
-
修饰符:抽象类必须用abstract修饰;普通类不能用abstract修饰。
简单说:抽象类是"模板",定义了子类的通用结构,子类必须补充完整抽象方法才能使用;普通类是"成品",可以直接使用。
七、Java抽象类和接口的区别是什么?
| 对比维度 | 抽象类(Abstract Class) | 接口(Interface) |
|---|---|---|
| 修饰符 | 用abstract修饰 | 用interface修饰 |
| 继承/实现 | 子类用extends继承,单继承 | 类用implements实现,多实现(一个类可实现多个接口) |
| 方法定义 | 可定义抽象方法、具体方法(有方法体) | JDK8前:只能定义抽象方法;JDK8后:可定义默认方法(default)、静态方法(static),无构造方法 |
| 属性定义 | 可定义普通属性、静态属性,可修改 | 只能定义常量(默认public static final),不可修改 |
| 实例化 | 不能直接实例化,子类继承后可实例化 | 不能实例化,只能由实现类实例化 |
| 核心作用 | 代码复用(子类继承通用属性和方法) | 定义规范(约束实现类必须实现的方法),实现多态 |
八、抽象类能加final修饰吗?
不能!核心原因:final修饰的类不能被继承,而抽象类的核心作用是被子类继承,子类重写其抽象方法才能使用;如果抽象类加final,就无法被继承,抽象方法也无法被重写,失去了抽象类的意义,编译器会直接报错。
补充:final可以修饰抽象类中的具体方法(表示该方法不能被子类重写),但不能修饰抽象类本身,也不能修饰抽象方法(抽象方法必须被重写,final禁止重写,冲突)。
九、接口里面可以定义哪些方法?
分JDK版本,面试要说明版本差异(避免踩坑),核心记JDK8及以后的情况:
-
JDK8之前:只能定义抽象方法(默认public abstract修饰,可省略),没有方法体,必须由实现类重写。
-
JDK8及以后:新增2种方法,加上抽象方法,共3种:
-
抽象方法:public abstract(可省略),无方法体,需实现类重写;
-
默认方法(default):有方法体,实现类可直接使用,也可重写(重写时不用加default);
-
静态方法(static):有方法体,只能通过接口名调用(比如InterfaceName.method()),实现类不能重写,也不能通过实现类对象调用。
-
补充:接口中的所有方法,默认都是public修饰(无论是否省略),不能用private、protected修饰。
十、抽象类可以被实例化吗?
不能直接实例化,但可以间接实例化:
-
直接实例化:用new关键字创建抽象类对象(比如new AbstractClass()),编译器直接报错,因为抽象类不完整(可能有抽象方法,没有方法体),无法直接使用。
-
间接实例化:创建抽象类的子类,子类重写抽象类中所有的抽象方法,然后实例化子类,本质上是通过子类实现抽象类的功能,间接使用抽象类(比如AbstractClass a = new SubClass(); 这里a是抽象类引用,指向子类对象)。
十一、接口可以包含构造函数吗?
不能!核心原因有2点:
-
构造函数的作用是初始化对象,而接口不能被实例化(没有对象需要初始化),所以构造函数没有意义。
-
接口的核心作用是定义规范,不负责具体实现,而构造函数是类的具体实现的一部分,与接口的设计初衷不符。
补充:抽象类可以有构造函数(用于子类继承时初始化父类的属性),但接口绝对没有,这也是抽象类和接口的重要区别之一。
十二、解释Java中的静态变量和静态方法
核心:静态变量和静态方法属于"类",不属于单个对象,所有对象共享,用static修饰,通俗解释+核心特点如下:
1. 静态变量(类变量)
定义:用static修饰的变量,属于整个类,不是某个对象的属性,所有对象共享同一个静态变量的值(比如Student类的static int count = 0; 所有Student对象的count都共享,一个对象修改,其他对象看到的也会变)。
特点:① 加载时机:类加载时就初始化,比对象创建早;② 调用方式:可通过类名调用(推荐,比如Student.count),也可通过对象调用;③ 内存存储:存放在方法区的静态区,只有一份。
2. 静态方法(类方法)
定义:用static修饰的方法,属于整个类,不依赖于对象,无需创建对象就能调用(比如Math.random(),不用new Math()就能调用)。
特点:① 不能访问非静态变量和非静态方法(因为非静态属于对象,静态方法加载时可能没有对象);② 可以访问静态变量和其他静态方法;③ 调用方式:类名.方法名(推荐),或对象.方法名。
补充:static不能修饰局部变量(比如方法内部不能定义static int a = 10;),只能修饰类级别的变量和方法。
十三、非静态内部类和静态内部类的区别
| 对比维度 | 非静态内部类(成员内部类) | 静态内部类(嵌套内部类) |
|---|---|---|
| 修饰符 | 无static修饰 | 有static修饰 |
| 依赖外部类 | 依赖外部类对象,必须先创建外部类对象,才能创建内部类对象 | 不依赖外部类对象,可直接通过外部类名创建内部类对象 |
| 访问外部类成员 | 可直接访问外部类的非静态成员和静态成员 | 只能访问外部类的静态成员,不能访问非静态成员 |
| 创建方式 | 外部类对象.new 内部类()(比如Outer outer = new Outer(); Outer.Inner inner = outer.new Inner();) | 外部类名.内部类()(比如Outer.Inner inner = new Outer.Inner();) |
| 是否有this引用 | 有两个this:this(内部类对象)、Outer.this(外部类对象) | 只有一个this(内部类对象),没有外部类this引用 |
十四、非静态内部类可以直接访问外部方法,编译器是如何做到的?
核心原理:编译器会自动为非静态内部类添加一个"外部类对象的引用",通过这个引用,非静态内部类就能直接访问外部类的成员(包括方法和属性),底层过程分3步:
-
编译阶段:当我们编写非静态内部类时,编译器会在内部类的构造方法中,悄悄添加一个参数------外部类对象的引用(比如Outer this$0),这个参数是隐式的,我们看不到,但确实存在。
-
对象创建阶段:当我们创建非静态内部类对象时(outer.new Inner()),编译器会自动将外部类对象(outer)作为参数,传递给内部类的构造方法,给this$0赋值,相当于"绑定"了外部类对象。
-
访问阶段:当非静态内部类访问外部类的方法时,底层会通过这个隐式的外部类引用(this0),调用外部类的方法(比如this0.outerMethod()),所以我们写代码时,不用手动调用,就能直接访问。
举个通俗例子:就像孩子(非静态内部类)天生知道自己的父母(外部类),不用父母主动告知,就能直接找父母帮忙(访问外部方法),这个"天生知道",就是编译器自动添加的外部类引用在起作用。
补充:正因为非静态内部类持有外部类的引用,如果内部类对象长期存活,而外部类对象本应被回收,就会导致内存泄漏(比如Android中,非静态内部类作为Handler,容易导致Activity内存泄漏)。