面向对象基础
面向对象和面向过程的区别
主要区别在于,对一个需求的解决方式不同。
-
面向过程:把要处理的需求分成多个步骤、多个方法,通过执行一个个方法来完成需求
-
面向对象:拿到一个需求,先设计抽象出多个类,用对象执行方法的方式完成需求,解决问题。
对象实体与对象引用有何不同?
(虽然心里知道有区别,但是一旦面试问到,大脑还是会一片空白,所以还是整理一下)
new 运算符,new 创建对象实例(对象实例在
内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
如果一个类没有声明构造方法,该程序能正常执行吗
可以。
-
如果没有手动添加构造方法,Java会默认添加一个无参的构造方法。这也就是为什么new一个对象的时候要带括号,实际上就是调用无参的构造方法。
-
如果自己手动写了有参的构造方法,Java就不会添加无参的构造方法了,所以一般如果自己定义了构造方法,无参的构造方法也要写出来。
构造方法有哪些特点,是否可以被override
特点:
-
方法名与类名相同
-
没有返回值,但也不能用void声明构造方法;
-
new一个对象的时候会自动执行,无需调用。
构造方法不可以被重写(因为构造方法的方法名与类名相同,而重写又是子类重写父类的方法,这就要求子类和父类的类名相同,显然不存在这种可能性。),但是可以被重载。
面向对象的三大特征
封装
将具有相同属性和行为的事物封装成一个类,隐藏了实现细节,外界不可以随意访问和修改。适当的封装可以让代码更容易理解和维护,也加强了代码的安全性。
继承
- 继承可以解决具有类似属性类的重复复制问题,如果有一个事物和已有的类相似,但不完全相同,这时候就可以使用继承,不需要重新创建一个相似的类。
- 特点
- 子类拥有父类中的一切,但是访问父类中的内容,需要受访问控制符的限制。而且不能继承父类的构造方法
- 子类可以定义自己的属性和方法,可以对父类进行扩展
- 子类可以覆盖父类的方法
多态
- 多态就是一个对象在调用方法的时候,不确定是调用的哪个方法。
- 多态分为静态多态(编译时多态)和动态多态(运行时多态)
- 静态多态:指方法的重载,具体调用哪个方法,是根据传入参数类型确定的。编译时可以知道调用的是哪个方法。
- 动态多态:如果子类重写了父类的方法,而且父类引用指向子类对象,那么在子类对象调用方法的时候,会出现多态的情况。子类对象可以调用哪些方法,是由引用类型决定的,实际调用哪个方法(子类还是父类的方法),是在代码执行过程中由子类对象类型决定的。
- 如果子类覆盖了父类的方法,那么调用的是子类的方法
- 如果子类没有重写父类的方法,就会去父类里找,父类没有就去父类的父类里找。
举例:
//queue对象能调用的方法就是队列的相关方法了,这就是一种多态
Queue<Integer> queue = new LinkedList<>()
//linkedList对象能调用的方法就不包含Queue中的方法了
LinkedList<Integer> linkedList = new LinkedList<>();
//deque对象能调用的方法就是Deque类中的方法
Deque<Integer> deque = new LinkedList<>();
多态的核心概念
为了更好地理解多态,我们需要掌握以下核心概念:
- 方法重写(Override): 子类可以提供对父类中已有方法的新实现。在子类中重新定义一个与父类中方法名、参数列表和返回类型相同的方法,从而覆盖(重写)了父类中的方法。
- 向上转型(Upcasting): 可以将子类的对象引用赋给父类类型的变量,这被称为向上转型。这样做可以让我们在父类引用上调用子类的方法,从而实现多态性。
- 动态绑定(Dynamic Binding): 运行时多态性的关键概念之一。它意味着方法的调用是在程序运行时根据对象的实际类型来确定的,而不是在编译时。
- instanceof 运算符: 用于检查一个对象是否是特定类的实例。它可以帮助我们在运行时确定对象的类型,从而进行适当的操作。
实现多态
要实现多态,需要满足以下条件:
- 存在继承关系,即有父类和子类。
- 子类必须重写父类的方法。这意味着子类提供了对父类方法的新实现。
- 父类的引用可以指向子类的对象,这是向上转型的体现。
重写和重载
什么是方法签名
方法的名称+参数列表(参数类型、顺序、数量),组成方法签名,一个类里面不能有多个签名相同的方法。
静态方法为什么不能调用非静态成员变量
静态方法跟类的生存周期一样,在类加载的时候就会分配内存;但非静态成员变量,跟对象的生存周期一样,类加载的时候,非静态成员变量还不存在,故此时静态方法调用非静态成员变量会报错(编译的时候就报错)。
方法的重载和重写的区别
- 重载:发生在同一个类或者子类和父类之间。有多个方法名称相同,参数类型/顺序/数量不同的方法
- 重写:发生在子类和父类之间。遵循“两同两小一大”
- 方法名、参数列表都必须相同;
- 子类返回值类型范围 小于等于父类的返回值类型范围
- 如果父类方法的返回值类型是void和基本类型,则子类方法的返回值类型不能改
- 如果父类方法的返回值类型是引用类型,则子类方法的返回值类型可以是该引用类型的子类
- 子类抛出的异常范围 小于等于父类的异常范围
- 子类方法的访问权限 大于等于父类方法的访问权限
什么是可变长参数
- 方法的形参个数不确定,调用方法时可以传入0-多个参数;如下:可以传入多个Integer类型或多个String类型
public String print(Integer... args){
return "789";
}
public String print(String... args){
return "123";
}
- 可变参数只能放在方法的最后一个参数;
- 遇到重载的情况,会优先匹配有固定参数的方法。
抽象类和接口的区别
接口的特点
接口中可以定义三种类型的方法
-
抽象方法:默认是public abstract修饰,修饰符可以不写。不能有方法体。子类必须重写该方法。
-
默认方法:使用default修饰(Java8),可以有方法体。子类可以不重写,子类对象会自动调用接口的default方法。
-
静态方法:使用public static修饰(Java8),可以有方法体,子类不可重写
接口中的默认方法、静态方法可以同时有多个。 参考文章:Java8:接口里面可以写实现方法吗【可以】 、接口可以多继承吗【可以】
接口中定义的成员变量
- 默认是public static final修饰的,也就是常量。
代码实践
public interface InterfaceTest {
// java 8之后,接口可以定义public static方法(private不允许),使用时直接InterfaceTest.staticMethod()进行调用即可
// Map接口下面就有许多静态方法,可以参考
static void staticMethod() {
System.out.println(123);
}
// java 8之后,接口可以定义default方法,使用时可通过子类对象(子类可以不重写该方法)调用该方法
// Map接口下面就有许多静态方法,可以参考
default String getName() {
return "123";
}
//接口一般定义的都是这样的抽象方法,不能有方法体。其中public abstract可以省略
public abstract void print();
}
子类实现上述接口
public class SubInterfaceTest implements InterfaceTest {
//子类必须实现接口的抽象方法
@Override
public void print() {
System.out.println("hidfhi");
}
public static void main(String[] args) {
//接口的静态方法,直接通过接口名称.方法名进行调用
InterfaceTest.staticMethod();
// 接口的default方法,通过子类对象调用
new SubInterfaceTest().getName();
}
}
为什么接口的静态和抽象方法必须是public修饰的
接口必须要有实现类才有意义,所以必须是public的
为什么接口的成员变量默认是public static final修饰
-
接口中的属性对所有实现类只有1份,所以是static的
-
要使实现类向上转型(父类引用指向子类对象)成功?,属性必须是final的。
为什么要有接口默认方法
因为接口的抽象方法是必须要被子类重写的。如果之前已经写好的接口,新加了一个抽象方法,那所有实现类都要重写该方法。而且有些实现类可能并不需要该方法,增加了代码的维护成本。接口里面的默认方法,实现类是可以自动继承的,不需要改动任何实现类,可以解决上述问题。
接口的默认方法,也可以被实现类重写。
为什么要有接口静态方法
接口静态方法和默认方法类似,只是不能被实现类重写。接口静态方法直接通过接口名.方法名来调用。
接口中的默认方法、静态方法可以同时有多个。 参考文章:Java8:接口里面可以写实现方法吗【可以】 、接口可以多继承吗【可以】
抽象类的特点
抽象类中可以定义任一类型的方法,包括:
- 抽象方法:默认是public abstract修饰,修饰符可以不写。不能有方法体。子类必须重写该方法。
- 普通方法:
- 静态方法:子类不可重写
抽象类中定义的成员变量
- 无控制符限制,可以定义private类型的变量。
总结
-
共同点:
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法
- 都可以定义public static的方法(可以有方法体)
-
区别:
- 作用不同:接口主要是对类的行为进行约束,实现了某个接口,就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 继承关系:一个类只能继承一个抽象类,但是可以实现多个接口。
- 成员变量不同:接口中的成员变量只能是public static final类型的。不能被修改而且必须有初始值。抽象类的成员变量无访问控制符限制,跟普通类的成员变量一样。
- 方法控制符不同:接口中方法的控制符默认是public,并且不能设置为其他控制符。抽象类中的方法控制符无限制,但抽象方法不能被private修饰(因为要给子类重写的,private修饰后,子类不能访问该方法了。)
- 静态代码块使用不同:接口中不能使用静态代码块;抽象类中可以。
子类是否可重写父类的静态方法
静态方法不存在是否重写的概念。 先看案例:
public class Fu {
public static void show() {
System.out.println("父类的静态方法");
}
public void method() {
System.out.println("父类的一般方法");
}
}
public class Zi extends Fu {
public static void main(String[] args) {
Fu fu = new Zi();
fu.show();
fu.method();
}
public static void show() {
System.out.println("子类的静态");
}
public void method() {
System.out.println("子类的一般方法");
}
}
输出结果是:
父类的静态方法
子类的一般方法
所以,当父类引用指向子类对象,只会调用父类的静态方法,此行为并不具有多态性!
为什么静态方法不存在重写的说法(待进一步整理)
静态方法在类加载时就分配了内存空间,存储在方法区(元空间)。子类中如果定义了相同名称的静态方法,并不会重写,而应该是在内存中又分配了一块给子类的静态方法,所以没有重写这一说,只是单纯的名字重复了。
从执行角度看:只要方法是静态的,那么在编译时就会直接调用方法区的静态方法,即使重写了也会去方法区,并不会去方法表找方法,重写实际是无效的,从这个角度看静态方法不能被重写。
(方法表满足两个特质:其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。)
参考文章:
访问控制符
四种访问控制符的修饰范围
| 修饰符 | 变量 | 方法 | 类 | 接口 |
|---|---|---|---|---|
| private | √ | √ | 只能修饰内部类 | × |
| default | × | 只能修饰接口中的方法 | × | × |
| protected | √ | √ | 只能修饰内部类 | × |
| public | √ | √ | √ | √ |
default关键字比较特殊:可以修饰变量、方法、类、接口。但是不能显示的修饰(除了接口中的default方法),也就是不能写上default关键字。如果上述四种内容,前面不加任何修饰符,则默认是default修饰的。
四种访问控制符的访问权限
| 修饰符 | 当前类 | 同一包 | 子孙类(同一包) | 子孙类(不同包) | 其他包 |
|---|---|---|---|---|---|
| private | √ | × | × | × | × |
| default | √ | √ | √ | × | × |
| protected | √ | √ | √ | × | × |
| public | √ | √ | √ | √ | √ |
protected和default修饰符需要特殊说明:
- 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。其实就是不能访问父类的protected方法。
代码实践
不相干的类,且不在同一包
public class SubAbstractClass extends AbstractClassTest {
// 前面不加任何修饰符,表明是default修饰的。如果前面加了default反而会报错。在其他包不能访问
String getNameDefault(){
return "Default";
}
// 前面不加任何修饰符,表明是default修饰的。在其他包不能访问
String nameDefault;
// 前面不加任何修饰符,表明是default修饰的。在其他包不能访问
class InnerClassDefault{
}
// protected修饰,其他包不能访问
protected String getNameProtected(){
return "Public";
}
// protected修饰,其他包不能访问
protected String nameProtected;
// protected修饰,其他包不能访问
protected class InnerClassProtected{
}
public String getNamePublic(){
return "Public";
}
public String namePublic;
public class InnerClassPublic{
}
}
在其他包的类中(非子孙类),访问上述default和protected修饰的内容。可以看到,只有public修饰的可以访问。
子孙类,不在同一包
public class SubClassProtectedTest extends SubAbstractClass {
protected String getNameProtected(){
return "123";
}
public static void main(String[] args) {
// 子类与父类不在同一包,无法直接访问父类实例的protected方法;因为编译会报错,所以注释掉
//new SubAbstractClass().getNameProtected();
// 子类只能访问从父类继承来的子类实例的方法
new SubClassProtectedTest().getNameProtected();
}
}
不相干的类,在同一包
可以看到,能访问同一包下的其他类的default、protected、public修饰的变量、方法。
深拷贝、浅拷贝、引用拷贝
浅拷贝
public class Address implements Cloneable {
private String name;
public Address(String name) {
this.name = name;
}
public Address() {
}
public Address clone() {
try {
// 直接调用Object类的clone方法,是浅拷贝
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address;
public Person(Address address) {
this.address = address;
}
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class PersonMain {
public static void main(String[] args) {
Person person1 = new Person(new Address("武汉"));
Person personCopy = person1.clone();
System.out.println(1);
}
}
上述main方法执行结果如下:
看上述对象的地址可以发现:person1的属性,是引用类型(除了基本类型就是引用类型),浅拷贝之后,创建了一个新的对象:personCopy,但是personCopy的属性地址仍和person1的相同。
故得出结论:浅拷贝会在堆上创建一个新的对象。不过,如果原对象内部的属性是引用类型,拷贝对象和源对象共用同一个内部对象。
深拷贝
对上述Person的clone方法做一些修改(为了演示方便,这里新建了一个PersonDeepCopy类)。
clone方法中,先调用Object的clone生成一个新的person对象。然后再获取person对象的address属性,再进行clone,即可获得一个新的address对象。将其赋值到person对象中,即可实现person对象的深拷贝。
@Data
public class PersonDeepCopy implements Cloneable {
private Address address;
public PersonDeepCopy(Address address) {
this.address = address;
}
public PersonDeepCopy clone() {
try {
PersonDeepCopy person = (PersonDeepCopy) super.clone();
// person.getAddress()先获取person里的引用对象,然后再对该引用类型进行clone,就可以实现深拷贝
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class PersonMain {
public static void main(String[] args) {
Person person1 = new Person(new Address("武汉"));
Person personShallowCopy = person1.clone();
PersonDeepCopy person2 = new PersonDeepCopy(new Address("南京"));
PersonDeepCopy personDeepCopy = person2.clone();
System.out.println(1);
}
}
上述Main方法的执行结果如下:
可以看到,从person2拷贝出来的personDeepCopy对象,是深拷贝,与原有对象不共用地址
引用拷贝
两个不同的引用,指向同一个对象。
this指针
。。。