1 面向对象三大特征
类是面向对象中一个重要的概念。类是具有相同属性和行为特征的对象的抽象,类是对象的概念模型,对象是类的一个实例,通过类来创建对象,同一类的所有对象具有相同的属性和行为特征。类具有三个基本特征:封装、继承、多态。
- 封装就是将对象的属性和行为特征包装到一个程序单元(即类)中,把实现细节隐藏起来,通过公用的方法来展现类对外提供的功能,提高了类的内聚性,降低了对象之间的耦合性。
- 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
- 多态是建立在继承的基础上的,是指父类引用指向子类对象,编译的时候看到的是父类,但运行时仍表现子类的行为特征。也就是说,同一种类型的对象执行同一个方法时可以表现出不同的行为特征。
2 简述重写和重载的区别
- 重写:存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
- 重写有三个限制:子类方法的访问权限必须大于等于父类方法;子类方法的返回类型必须是父类方法返回类型或为其子类型;子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
- 使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
重载:存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
被 abstract 关键字修饰。
- 含有抽象方法的类一定是抽象类,但是抽象类不一定含有抽象方法;且抽象方法必须是 public 或 protected,否则不能被子类继承。默认修饰符根据 Java 版本而定。
- 抽象方法可以有具体数据和具体实现。
- 抽象类中可以定义自己的成员变量,权限没要求,private,protected,public。
- 抽象类不能用 static 和 private 修饰,因为其中的抽象方法需要去实现。
- 子类继承抽象类时,必须实现抽象类中的所有抽象方法,否则该子类也要被定义为抽象类。
- 抽象类不能被实例化,只能引用其非抽象子类的对象。
- 可以有构造器,初始化块,内部类。
- abstract 不能修饰成员,局部变量。 ```java package org.example;
public class App { public static void main(String[] args) { A1 a = new A2(2, 3, 4); a.method1(); a.show(); /**输出结果:
* 父类的静态初始化块
* 子类的静态初始化块
* 父类的普通初始化块
* 父类的构造方法
* 子类的普通初始化块
* 子类的构造方法
* 子类实现的抽象方法
* 父类的内部类
* 2+3+4
*/
}
} // 抽象类 abstract class A1 { static { System.out.println(“父类的静态初始化块”); }
{
System.out.println("父类的普通初始化块");
}
// 成员变量
private int a = 0;
protected int b = 1;
public int c = 2;
// 抽象方法
public abstract void method1();
// 构造方法
public A1(int a, int b, int c) {
this.a = a;
this.b = b;
this.c = c;
System.out.println("父类的构造方法");
}
// 公共方法
public void show() {
InnerA1 innerA1 = new InnerA1();
innerA1.show();
}
// 内部类
class InnerA1 {
public void show() {
System.out.println("父类的内部类");
System.out.println(a + "+" + b + "+" + c);
}
}
}
// 非抽象子类需要实现父类的抽象方法 class A2 extends A1 { static { System.out.println(“子类的静态初始化块”); }
{
System.out.println("子类的普通初始化块");
}
public A2(int a, int b, int c) {
super(a, b, c);
System.out.println("子类的构造方法");
}
// 实现父类的抽象方法
@Override
public void method1() {
System.out.println("子类实现的抽象方法");
}
}
<a name="A0Wx1"></a>
## 5 接口的相关特性
- 接口中变量类型默认且只能是 public staic final。
- 接口中声明抽象方法,且只能是默认的public abstract,没有具体的实现,默认方法没有方法体,但JDK1.8之后有默认方法,静态方法是要有方法体。
- 子类必须实现所有接口函数。
- 不能定义构造器和初始化块。
- 接口可多继承。
- 接口的实现类必须全部实现接口中的方法,如果不实现,可以将子类变成一个抽象类。
<a name="mOY6H"></a>
## 6 接口和抽象类的区别
| | 抽象类 | 接口 |
| --- | --- | --- |
| 是否可以多继承 | 不可以 | 可以 |
| 是否有构造器 | 可以有 | 不可以 |
| 是否有静态代码块和静态方法 | 可以有 | 不可以 |
| 实现方式 | extends,只能单继承 | implements,可以多实现 |
| 抽象方法 | 可以是public、protected | 只能是public abstract |
| 成员变量 | 可以是任意类型 | 只能是public static final |
| 成员方法 | 可以拥有普通的成员方法 | 不能有 |
从实现方式;多继承;是否有构造器;是否有静态块;是否有静态方法;成员变量修饰符;成员方法修饰符;是否有默认实现;抽象方法是否必须全部实现;是否可实例化等方面回答。
<a name="3JhRY"></a>
## 7 接口与抽象类在不同版本中的变化
- 抽象类在1.8以前,其方法的默认访问权限为protected;1.8后改为default
- 接口在1.8以前,方法必须是public;1.8时可以使用default;1.9时可以是private
<a name="fdXNd"></a>
## 8 通过实例对象.方法名这种调用过程的流程
- 编译器查看对象的声明类型和方法名;如果有多个同名但参数类型不同的函数,那么编译器将一一列举所有该类中同名的方法和超类中同名的方法。
- 编译器查看调用方法时提供的参数类型。如果存在一个参数匹配的方法,那么就使用这个方法,这个过程称之为重载解析;如果未找到一个匹配的参数,那么就会报错。
- 如果该方法是 private 方法、 static 方法、 final 方法或者构造器,则编译器可以准确地知道应该调用哪种方法,被称之为静态绑定。与之对应的是,调用方法依赖于隐式参数的实际类型,并在运行时实现动态绑定。
- 当程序运行时并采用动态绑定调用方法时,虚拟机一定会调用最合适的那个类方法,否则层层向超类上搜索
值得注意的是,在调用方法搜索时,时间开销相当大。因此 ,虚拟机为每个类预先创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。通过查表节省时间。
<a name="qXgdE"></a>
## 9 为什么在父类中要定义一个没有参数的空构造函数
Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会默认调用父类中“没有参数的构造方法”。<br />因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。<br />解决办法是在父类里加上一个不做事且没有参数的构造方法。
```java
public class Main {
public static void main(String[] args) {
Parent dp = new Son(12);
}
}
class Parent {
int age;
// 注释无参构造函数,无法通过编译
Parent() {}
Parent(int age) {
this.age = age;
}
}
class Son extends Parent {
int height;
Son(int height) {
super();
this.height = height;
}
}
10 简述静态类和单例的区别
- 单例可以继承类,实现接口,而静态类不能。
- 单例可以被延迟初始化,静态类一般在第一次加载是初始化。
- 单例类可以被继承,它的方法可以被覆写。
- 单例类可以被用于多态而无需强迫用户只假定唯一的实例。
- 静态方法中产生的对象,会随着静态方法执行完毕而释放掉,而且执行类中的静态方法时,不会实例化静态方法所在的类。如果是用singleton,产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个JVM退出。
// 线程安全的单例类实现
public final class Singleton {
private Singleton() {};
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == NULL) {
synchronized(Singleton.class) {
if (INSTANCE == NULL) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
11 成员变量与局部变量
- 成员变量可以使用 public,private,static等修饰符修饰;而局部变量不能被访问修饰符以及static 修饰。二者均可被final修饰。
- 成员变量在堆中,而局部变量在栈中。
成员变量如果未被赋初值,则会自动以默认值赋值,final修饰则需要显式地手动赋值;而局部变量不会被自动赋初值,直接拿来用会抛异常。
12 静态方法和实例方法的区别
在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
13 为什么Java中只有值传递
在程序设计语言中存在两种传递方式,分别是传值调用和传址调用。传值调用指的是方法接收的是调用者提供的值,而传址调用指的是调用者提供变量的地址。一个方法可以修改传址调用所对应的变量值,而不能修改传值调用所对应的变量值。
Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。所以得到如下结论:在其他方法里面改变引用类型的值肯定是通过引用改变的,传递引用对象的时候传递的是复制过的对象句柄(引用),注意这个引用是复制过的,也就是说又在内存中复制了一份句柄,这时候有两个句柄是指向同一个对象的,所以你改变这个句柄对应空间的数据会影响外部的变量的,当你把这个句柄指向其他对象的引用时并不会改变原对象。
- 在传递基本类型时,传递的是值,因此需要看改变这个“值”对原数据的影响。
- 在传递引用类型时,传递的是地址值,因此需要看的是改变这个“地址值”对原数据的影响,而不是看改变这个“地址值所在的数据”对原数据的影响。
来看一下三个例子:
- 基本数据类型作为参数 ```java package org.example;
public class Demo1 { public static void main(String[] args) { int num1 = 1; int num2 = 2; swap(num1, num2); System.out.println(“num1 = “ + num1); System.out.println(“num2 = “ + num2); }
public static void swap(int a, int b) {
int c = a;
a = b;
b = c;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
执行结果:
```java
a = 2
b = 1
num1 = 1
num2 = 2
在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。
基本数据类型作为参数,不会被一个方法所修改。
- 引用类型作为参数 ```java package org.example;
public class Demo2 { public static void main(String[] args) { int arr[] = {1, 2, 3}; System.out.println(arr[0]); swap(arr); System.out.println(arr[0]); }
private static void swap(int[] array) {
array[0] = 999;
System.out.println(array[0]);
}
}
执行结果:
```java
1
999
999
arr变量存放的实际上是一个地址值,指向一个数组对象。而array是arr的一个拷贝,也是一个地址值。所以arr和array实际上指向的是同一个对象,对array的修改也会在arr中显示。
引用类型的参数,可以被一个方法修改。
- 对象作为参数 ```java package org.example;
public class Demo3 { public static void main(String[] args) { Student s1 = new Student(“zhangsan”, 1); Student s2 = new Student(“lisi”, 2);
System.out.println(s1.getName() + ", 11111111");
System.out.println(s2.getName() + ", 11111111");
System.out.println("------------");
swap(s1, s2);
System.out.println(s1.getName() + ", 33333333");
System.out.println(s2.getName() + ", 33333333");
}
private static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println(x.getName() + ", 22222222");
System.out.println(y.getName() + ", 22222222");
System.out.println("------------");
}
private static class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
}
}
执行结果:
```java
zhangsan, 11111111
lisi, 11111111
------------
lisi, 22222222
zhangsan, 22222222
------------
zhangsan, 33333333
lisi, 33333333
可以看到,swap 方法内部已经交换了 x 和 y,但是 s1 和 s2 没有被交换。这是因为,x 和 y 接收的是 s1 和 s2 存放的地址值的副本。swap 方法里面是 x 和 y 进行交换,也就是 x 和 y 发生了地址值副本的交换,而 s1 和 s2 所存放的值没有任何改,所以不会影响到 s1 和 s2。
举个例子:
- 甲知道图书馆的地址,乙知道操场的地址。
- 甲把图书馆的地址告诉了丙,乙把操场的地址告诉了丁。
- 丙和丁之间进行了信息的交换。这样丙就知道去操场的位置,丁就知道去图书馆的位置。
- 甲 和 乙 并没有任何改变。因为交换的,只是地址的副本。
参考
- https://blog.csdn.net/bjweimengshu/article/details/79799485
- https://snailclimb.gitee.io/javaguide/#/docs/java/basis/Java%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86?id=java-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1
- https://www.pdai.tech/md/java/basic/java-basic-oop.html
- https://imlql.cn/post/5df2d017.html
- https://blog.nowcoder.net/n/8f0280724e074093a7e7b5951098c2bc