1 面向对象三大特征

类是面向对象中一个重要的概念。类是具有相同属性和行为特征的对象的抽象,类是对象的概念模型,对象是类的一个实例,通过类来创建对象,同一类的所有对象具有相同的属性和行为特征。类具有三个基本特征:封装、继承、多态。

  • 封装就是将对象的属性和行为特征包装到一个程序单元(即类)中,把实现细节隐藏起来,通过公用的方法来展现类对外提供的功能,提高了类的内聚性,降低了对象之间的耦合性。
  • 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
  • 多态是建立在继承的基础上的,是指父类引用指向子类对象,编译的时候看到的是父类,但运行时仍表现子类的行为特征。也就是说,同一种类型的对象执行同一个方法时可以表现出不同的行为特征。

继承相关难题

2 简述重写和重载的区别

  • 重写:存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
    • 重写有三个限制:子类方法的访问权限必须大于等于父类方法;子类方法的返回类型必须是父类方法返回类型或为其子类型;子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
    • 使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
  • 重载:存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同

    • 返回值不同,其它都相同不算是重载。

      3 构造器可以重写么

      父类的构造器和私有属性不能被继承,所以无法重写,但是构造器其本身可以进行重载,即一个类里有多个构造函数的情况。

      4 抽象类的相关特性

  • 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(); /**输出结果:

  1. * 父类的静态初始化块
  2. * 子类的静态初始化块
  3. * 父类的普通初始化块
  4. * 父类的构造方法
  5. * 子类的普通初始化块
  6. * 子类的构造方法
  7. * 子类实现的抽象方法
  8. * 父类的内部类
  9. * 2+3+4
  10. */
  11. }

} // 抽象类 abstract class A1 { static { System.out.println(“父类的静态初始化块”); }

  1. {
  2. System.out.println("父类的普通初始化块");
  3. }
  4. // 成员变量
  5. private int a = 0;
  6. protected int b = 1;
  7. public int c = 2;
  8. // 抽象方法
  9. public abstract void method1();
  10. // 构造方法
  11. public A1(int a, int b, int c) {
  12. this.a = a;
  13. this.b = b;
  14. this.c = c;
  15. System.out.println("父类的构造方法");
  16. }
  17. // 公共方法
  18. public void show() {
  19. InnerA1 innerA1 = new InnerA1();
  20. innerA1.show();
  21. }
  22. // 内部类
  23. class InnerA1 {
  24. public void show() {
  25. System.out.println("父类的内部类");
  26. System.out.println(a + "+" + b + "+" + c);
  27. }
  28. }

}

// 非抽象子类需要实现父类的抽象方法 class A2 extends A1 { static { System.out.println(“子类的静态初始化块”); }

  1. {
  2. System.out.println("子类的普通初始化块");
  3. }
  4. public A2(int a, int b, int c) {
  5. super(a, b, c);
  6. System.out.println("子类的构造方法");
  7. }
  8. // 实现父类的抽象方法
  9. @Override
  10. public void method1() {
  11. System.out.println("子类实现的抽象方法");
  12. }

}

  1. <a name="A0Wx1"></a>
  2. ## 5 接口的相关特性
  3. - 接口中变量类型默认且只能是 public staic final。
  4. - 接口中声明抽象方法,且只能是默认的public abstract,没有具体的实现,默认方法没有方法体,但JDK1.8之后有默认方法,静态方法是要有方法体。
  5. - 子类必须实现所有接口函数。
  6. - 不能定义构造器和初始化块。
  7. - 接口可多继承。
  8. - 接口的实现类必须全部实现接口中的方法,如果不实现,可以将子类变成一个抽象类。
  9. <a name="mOY6H"></a>
  10. ## 6 接口和抽象类的区别
  11. | | 抽象类 | 接口 |
  12. | --- | --- | --- |
  13. | 是否可以多继承 | 不可以 | 可以 |
  14. | 是否有构造器 | 可以有 | 不可以 |
  15. | 是否有静态代码块和静态方法 | 可以有 | 不可以 |
  16. | 实现方式 | extends,只能单继承 | implements,可以多实现 |
  17. | 抽象方法 | 可以是public、protected | 只能是public abstract |
  18. | 成员变量 | 可以是任意类型 | 只能是public static final |
  19. | 成员方法 | 可以拥有普通的成员方法 | 不能有 |
  20. 从实现方式;多继承;是否有构造器;是否有静态块;是否有静态方法;成员变量修饰符;成员方法修饰符;是否有默认实现;抽象方法是否必须全部实现;是否可实例化等方面回答。
  21. <a name="3JhRY"></a>
  22. ## 7 接口与抽象类在不同版本中的变化
  23. - 抽象类在1.8以前,其方法的默认访问权限为protected;1.8后改为default
  24. - 接口在1.8以前,方法必须是public;1.8时可以使用default;1.9时可以是private
  25. <a name="fdXNd"></a>
  26. ## 8 通过实例对象.方法名这种调用过程的流程
  27. - 编译器查看对象的声明类型和方法名;如果有多个同名但参数类型不同的函数,那么编译器将一一列举所有该类中同名的方法和超类中同名的方法。
  28. - 编译器查看调用方法时提供的参数类型。如果存在一个参数匹配的方法,那么就使用这个方法,这个过程称之为重载解析;如果未找到一个匹配的参数,那么就会报错。
  29. - 如果该方法是 private 方法、 static 方法、 final 方法或者构造器,则编译器可以准确地知道应该调用哪种方法,被称之为静态绑定。与之对应的是,调用方法依赖于隐式参数的实际类型,并在运行时实现动态绑定。
  30. - 当程序运行时并采用动态绑定调用方法时,虚拟机一定会调用最合适的那个类方法,否则层层向超类上搜索
  31. 值得注意的是,在调用方法搜索时,时间开销相当大。因此 ,虚拟机为每个类预先创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。通过查表节省时间。
  32. <a name="qXgdE"></a>
  33. ## 9 为什么在父类中要定义一个没有参数的空构造函数
  34. Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会默认调用父类中“没有参数的构造方法”。<br />因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。<br />解决办法是在父类里加上一个不做事且没有参数的构造方法。
  35. ```java
  36. public class Main {
  37. public static void main(String[] args) {
  38. Parent dp = new Son(12);
  39. }
  40. }
  41. class Parent {
  42. int age;
  43. // 注释无参构造函数,无法通过编译
  44. Parent() {}
  45. Parent(int age) {
  46. this.age = age;
  47. }
  48. }
  49. class Son extends Parent {
  50. int height;
  51. Son(int height) {
  52. super();
  53. this.height = height;
  54. }
  55. }

10 简述静态类和单例的区别

  • 单例可以继承类,实现接口,而静态类不能。
  • 单例可以被延迟初始化,静态类一般在第一次加载是初始化。
  • 单例类可以被继承,它的方法可以被覆写。
  • 单例类可以被用于多态而无需强迫用户只假定唯一的实例。
  • 静态方法中产生的对象,会随着静态方法执行完毕而释放掉,而且执行类中的静态方法时,不会实例化静态方法所在的类。如果是用singleton,产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个JVM退出。

单例类的五种写法

  1. // 线程安全的单例类实现
  2. public final class Singleton {
  3. private Singleton() {};
  4. private static volatile Singleton INSTANCE = null;
  5. public static Singleton getInstance() {
  6. if (INSTANCE == NULL) {
  7. synchronized(Singleton.class) {
  8. if (INSTANCE == NULL) {
  9. INSTANCE = new Singleton();
  10. }
  11. }
  12. }
  13. return INSTANCE;
  14. }
  15. }

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); }

  1. public static void swap(int a, int b) {
  2. int c = a;
  3. a = b;
  4. b = c;
  5. System.out.println("a = " + a);
  6. System.out.println("b = " + b);
  7. }

}

  1. 执行结果:
  2. ```java
  3. a = 2
  4. b = 1
  5. num1 = 1
  6. 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]); }

  1. private static void swap(int[] array) {
  2. array[0] = 999;
  3. System.out.println(array[0]);
  4. }

}

  1. 执行结果:
  2. ```java
  3. 1
  4. 999
  5. 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);

  1. System.out.println(s1.getName() + ", 11111111");
  2. System.out.println(s2.getName() + ", 11111111");
  3. System.out.println("------------");
  4. swap(s1, s2);
  5. System.out.println(s1.getName() + ", 33333333");
  6. System.out.println(s2.getName() + ", 33333333");
  7. }
  8. private static void swap(Student x, Student y) {
  9. Student temp = x;
  10. x = y;
  11. y = temp;
  12. System.out.println(x.getName() + ", 22222222");
  13. System.out.println(y.getName() + ", 22222222");
  14. System.out.println("------------");
  15. }
  16. private static class Student {
  17. private String name;
  18. private int age;
  19. public Student(String name, int age) {
  20. this.name = name;
  21. this.age = age;
  22. }
  23. public String getName() {
  24. return name;
  25. }
  26. }

}

  1. 执行结果:
  2. ```java
  3. zhangsan, 11111111
  4. lisi, 11111111
  5. ------------
  6. lisi, 22222222
  7. zhangsan, 22222222
  8. ------------
  9. zhangsan, 33333333
  10. lisi, 33333333

可以看到,swap 方法内部已经交换了 x 和 y,但是 s1 和 s2 没有被交换。这是因为,x 和 y 接收的是 s1 和 s2 存放的地址值的副本。swap 方法里面是 x 和 y 进行交换,也就是 x 和 y 发生了地址值副本的交换,而 s1 和 s2 所存放的值没有任何改,所以不会影响到 s1 和 s2。
举个例子:

  • 甲知道图书馆的地址,乙知道操场的地址。
  • 甲把图书馆的地址告诉了丙,乙把操场的地址告诉了丁。
  • 丙和丁之间进行了信息的交换。这样丙就知道去操场的位置,丁就知道去图书馆的位置。
  • 甲 和 乙 并没有任何改变。因为交换的,只是地址的副本。

参考