java面试必备--JAVA基础篇(三) 之 面向对象

      相信很多同行小伙伴会因为许多原因想跳槽,不论是干得不开心还是想跳槽涨薪,在如此内卷的行业,我们都面临着“面试造火箭,上班拧螺丝”的局面,鉴于当前形势博主呕心沥血整理的干货满满的造火箭的技巧来了,本博主花费2个月时间,整理归纳java全生态知识体系常见面试题!总字数高达百万! 干货满满,每天更新,关注我,不迷路,用强大的归纳总结,全新全细致的讲解来留住各位猿友的关注,希望能够帮助各位猿友在应付面试笔试上!当然如有归纳总结错误之处请各位指出修正!如有侵权请联系博主QQ1062141499! 
   

目录

     

1 说说你对面向对象的理解

  1.1 对象有以下特点:

  1.2 面向对象的特性:

2 面向对象都有哪些特性

3 面向对象设计原则有哪些?

4 面向对象设计的七大原则

  4.1.开闭原则 - Open Close Principle(OCP)

      1)定义

      2)基本概念

     3)优点

     4)示例:

2.单一职责原则 - Single Responsibility Principle(SRP)

  1)定义

  2)基本概念

  3)优点

  4)示例:现在,以动物为例说明什么是单一原则

3.里士替换原则 - Liskov Substitution Principle(LSP)

   1)定义

   2)基本概念

  3)优点

  4)示例:

4.依赖倒置原则 - Dependence Inversion Principle(DIP)

1)定义

2)基本概念

3)优点

4)示例:

5.接口隔离原则 - Interface Segration Principle(ISP)

1)定义

2)基本概念

3)优点

4)代码示例:

6.迪米特法则/最少知道原则 - Law of Demeter or Least Knowledge Principle(LoD or LKP)

1)定义

2)基本概念

3)优点

4)详细讲解:

7.合成/聚合复用原则 - Composite/Aggregate Reuse Principle(CARP / CRP)

1) 定义

2)基本概念

3)优点

4)讲解

5 什么是Java的多态?

6 Java中实现多态的机制是什么?

7 类和对象的关系

8 说一说你的对面向过程和面向对象的理解

9 抽象类必须要有抽象方法吗?

10 普通类和抽象类区别?

11 JDK8 中为什么有接口默认方法

12 接口和抽象类区别

13 抽象类能使用final修饰吗?

14  abstract方法是否可是static的?native的?synchronized的?

15 重载(overload)和重写(override)的区别?

16 子类构造方法的执行过程是什么样的?

17 为什么函数不能根据返回类型来区分重载

18 为什么Java中不支持多重继承?

19 为什么Java不支持运算符重载?

20 你能用Java覆盖静态方法吗?如果我在子类中创建相同的方法是编译时错误?

21 类的实例化方法调用顺序

22 实例方法和类方法


1 说说你对面向对象的理解

     对 Java 语言来说,一切皆是对象。

  1.1 对象有以下特点

  1. 对象具有属性和行为
  2. 对象具有变化的状态
  3. 对象具有唯一性
  4. 对象都是某个类别的实例
  5. 一切皆为对象,真实世界中的所有事物都可以视为对象

  1.2 面向对象的特性

  1. 抽象性:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。
  2. 继承性:指子类拥有父类的全部特征和行为,这是类之间的一种关系。Java 只支持单继承。
  3. 封装性:封装是将代码及其处理的数据绑定在一起的一种编程机制,该机制保证了程序和数据都不受外部干扰且不被误用。封装的目的在于保护信息。
  4. 多态性:多态性体现在父类的属性和方法被子类继承后或接口被实现类实现后,可以具有不同的属性或表现方式。

2 面向对象都有哪些特性

     1继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。

     2封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。

    3多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当A系统访问B系统提供的服务时,B系统有多种提供服务的方式,但一切对 A 系统来说都是透明的。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:

          1. 方法重写(子类继承父类并重写父类中已有的或抽象的方法);

           2. 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

    4抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

      注意:默认情况下面向对象有3大特性,封装、继承、多态,如果面试官问让说出4大特性,那么我们就把抽象加上去。

3 面向对象设计原则有哪些?

  1. 单一职责原则 SRP
  2. 开闭原则 OCP
  3. 里氏替代原则 LSP
  4. 依赖注入原则 DIP
  5. 接口分离原则 ISP
  6. 迪米特原则 LOD
  7. 组合/聚合复用原则 CARP

其他原则可以看作是开闭原则的实现手段或方法,开闭原则是理想状态

4 面向对象设计的七大原则

  4.1.开闭原则 - Open Close Principle(OCP)

      1)定义

  • 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
  • Software entities like classes,modules and functions should be open for extension but closed for modifications.

      2)基本概念

  • 开:对扩展开放,支持方便的扩展
  • 闭:对修修改关闭,严格限制对已有的内容修改
  • 说明:一个软件产品只要在生命周期内,都会发生变化,即然变化是一个事实,我们就应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改现有代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则

     3)优点

  • 提高系统的灵活性、可复用性和可维护性

     4)示例:

       现在,以课程为例说明什么是开闭原则

/**
* 定义课程接口
*/
public interface ICourse {
    String getName();  // 获取课程名称
    Double getPrice(); // 获取课程价格
    Integer getType(); // 获取课程类型
}
/**
 * 英语课程接口实现
 */
public class EnglishCourse implements ICourse {
    private String name;
    private Double price;
    private Integer type;
    public EnglishCourse(String name, Double price, Integer type) {
        this.name = name;
        this.price = price;
        this.type = type;
    }
    @Override
    public String getName() {
        return null;
    }
    @Override
    public Double getPrice() {
        return null;
    }
    @Override
    public Integer getType() {
        return null;
    }
}
// 测试
public class Main {
    public static void main(String[] args) {
        ICourse course = new EnglishCourse("小学英语", 199D, "Mr.Zhang");
        System.out.println(
                "课程名字:"+course.getName() + " " +
                "课程价格:"+course.getPrice() + " " +
                "课程作者:"+course.getAuthor()
        );
    }
}

项目上线,课程正常销售,但是我们产品需要做些活动来促进销售,比如:打折。那么问题来了:打折这一动作就是一个变化,而我们要做的就是拥抱变化,现在开始考虑如何解决这个问题,可以考虑下面三种方案:

     1)修改接口

  • 在之前的课程接口中添加一个方法 getSalePrice() 专门用来获取打折后的价格;
  • 如果这样修改就会产生两个问题,所以此方案否定
  •      (1) ICourse 接口不应该被经常修改,否则接口作为契约的作用就失去了
  •      (2) 并不是所有的课程都需要打折,加入还有语文课,数学课等都实现了这一接口,但是只有英语课打折,与实际业务不符合。
public interface ICourse {
    // 获取课程名称
    String getName();
    // 获取课程价格
    Double getPrice();
    // 获取课程类型
    String getAuthor();
    
    // 新增:打折接口
    Double getSalePrice();
}

2)修改实现类

  • 在接口实现里直接修改 getPrice()方法,此方法会导致获取原价出问题;或添加获取打折的接口 getSalePrice(),这样就会导致获取价格的方法存在两个,所以这个方案也否定,此方案不贴代码了。

3)通过扩展实现变化

  • 直接添加一个子类 SaleEnglishCourse ,重写 getPrice()方法,这个方案对源代码没有影响,符合开闭原则,所以是可执行的方案,代码如下,代码如下:
public class SaleEnglishCourse extends EnglishCourse {
    public SaleEnglishCourse(String name, Double price, String author) {
        super(name, price, author);
    }
    @Override
    public Double getPrice() {
        return super.getPrice() * 0.85;
    }
}

    综上所述,如果采用第三种,即开闭原则,以后再来个语文课程,数学课程等等的价格变动都可以采用此方案,维护性极高而且也很灵活

2.单一职责原则 - Single Responsibility Principle(SRP)

  1)定义

  • 不要存在多于一个导致类变更的原因
  • There should never be more than one reason for a class to change.

  2)基本概念

  • 单一职责是高内聚低耦合的一个体现
  • 说明:通俗的讲就是一个类只能负责一个职责,修改一个类不能影响到别的功能,也就是说只有一个导致该类被修改的原因

  3)优点

  • 低耦合性,影响范围小
  • 降低类的复杂度,职责分明,提高了可读性
  • 变更引起的风险低,利于维护

  4)示例:现在,以动物为例说明什么是单一原则

     假如说,类 A 负责两个不同的职责,T1 和 T2,当由于职责 T1 需求发生改变而需要修改类 A 时,有可能会导致原本运行正常的职责 T2 功能发生改变或出现异常。为什么会出现这种问题呢?代码耦合度太高,实现复杂,简单一句话就是:不够单一。那么现在提出解决方案:分别建立两个类 A 和 B ,使 A 完成职责 T1 功能,B 完成职责 T2 功能,这样在修改 T1 时就不会影响 T2 了,反之亦然。

      说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责 T 被分化为粒度更细的职责 T1 和 T2

/**
* 定义动物类
*/
public class Animal {
    public void move(String animal){
        System.out.println(animal + "用翅膀飞");
    }
}
/**
 * 第一次测试
 */
public class Main {
    public static void main(String[] args) {
       Animal animal = new Animal();
       animal.move("麻雀");
       animal.move("老鹰");
       animal.move("鲸鱼");
    }
}

     经过上面代码示例发现:麻雀和老鹰会飞是可以理解的,但是鲸鱼就有点不合常理了,那么我们遵循单一对代码进行第(一)次修改,代码如下:

/**
 * 会飞的动物
 */
public class FlyAnimal {
    public void move(String animal){
        System.out.println(animal + "用翅膀飞");
    }
}
/**
 * 在水里的动物
 */
public class WaterAnimal {
    public void move(String animal){
        System.out.println(animal + "在水里游泳");
    }
}
/**
 * 测试
 */
public class Main {
    public static void main(String[] args) {
       FlyAnimal flyAnimal = new FlyAnimal();
       flyAnimal.move("麻雀");
       flyAnimal.move("老鹰");
       WaterAnimal waterAnimal = new WaterAnimal();
        waterAnimal.move("鲸鱼");
    } 
}

     遵循单一原则发现确实是职责单一了,但是我们会发现如果这样修改花销是很大的,除了将原来的类分解之外还需要修改客户端代码。如果我们不去遵循单一原则,而是直接在原有代码进行第(二)次修改,代码如下:

/**
 * 直接修改源代码
 */
public class Animal {
    public void move(String animal){
        if ("鲸鱼".equals(animal)) {
            System.out.println(animal + "在水里游泳");
        } else {
            System.out.println(animal + "用翅膀飞");
        }
    }
}
/**
 * 第三次测试
 */
public class Main {
    public static void main(String[] args) {
       Animal animal = new Animal();
       animal.move("麻雀");
       animal.move("老鹰");
       animal.move("鲸鱼");
    }
}

     直接修改源码确实简单很多,但是却存在巨大的隐患,加入需求变了:将在水里的动物分为淡水的和海水的,那么又要继续修改 Animal 类的 move() 方法,这也就对会飞的动物造成了一定的风险,所以我们继续摒弃单一原则对代码进行第(三)次修改,代码如下:

public class Animal {
    public void move(String animal){
        System.out.println(animal + "在水里游泳");
    }
    public void moveA(String animal){
        System.out.println(animal + "用翅膀飞");
    }
}
public class Main {
    public static void main(String[] args) {
       Animal animal = new Animal();
       animal.move("麻雀");
       animal.move("老鹰");
       animal.moveA("鲸鱼");
    }
}

    在最后一种修改中可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。综上所述,这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?结论:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;

3.里士替换原则 - Liskov Substitution Principle(LSP)

   1)定义

  • 定义一:所有引用基类的地方必须能透明地使用其子类的对象。
  • 定义二:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
  • Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

   2)基本概念

  • 强调的是设计和实现要依赖于抽象而非具体;子类只能去扩展基类,而不是隐藏或者覆盖基类它包含以下4层含义

      1)子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

      2)子类中可以增加自己特有的方法。

      3)当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

     4)当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

  3)优点

  • 开闭原则的体现,约束继承泛滥
  • 提高系统的健壮性、扩展性和兼容性

  4)示例:

     代码讲解第三个概念 :

     当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松

public class ParentClazz {
    public void say(CharSequence str) {
        System.out.println("parent execute say " + str);
    }
}
public class ChildClazz extends ParentClazz {
    public void say(String str) {
        System.out.println("child execute say " + str);
    }
}
/**
 * 测试
 */
public class Main {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        ParentClazz parent = new ParentClazz();
        parent.say("hello");
        ChildClazz child = new ChildClazz();
        child.say("hello");
    }
}
执行结果:
parent execute say hello
child execute say hello

    以上代码中我们并没有重写父类的方法,只是重载了同名方法,具体的区别是:子类的参数 String 实现了父类的参数 CharSequence。此时执行了子类方法,在实际开发中,通常这不是我们希望的,父类一般是抽象类,子类才是具体的实现类,如果在方法调用时传递一个实现的子类可能就会产生非预期的结果,引起逻辑错误,根据里士替换原则的子类的输入参数要宽于或者等于父类的输入参数,我们可以修改父类参数为String,子类采用更宽松的 CharSequence,如果你想让子类的方法运行,就必须覆写父类的方法。代码如下:

public class ParentClazz {
    public void say(String str) {
        System.out.println("parent execute say " + str);
    }
}
public class ChildClazz extends ParentClazz {
    public void say(CharSequence str) {
        System.out.println("child execute say " + str);
    }
}
public class Main {
    public static void main(String[] args) {
        ParentClazz parent = new ParentClazz();
        parent.say("hello");
        ChildClazz child = new ChildClazz();
        child.say("hello");
    }
}
执行结果:
parent execute say hello
parent execute say hello

代码讲解第四个概念 :

public abstract class Father {
    public abstract Map hello();
}
public class Son extends Father {
    @Override
    public Map hello() {
        HashMap map = new HashMap();
        System.out.println("son execute");
        return map;
    }
}
public class Main {
    public static void main(String[] args) {
        Father father = new Son();
        father.hello();
    }
}
执行结果:
son execute

4.依赖倒置原则 - Dependence Inversion Principle(DIP)

1)定义

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
  • High level modules should not depend upon low level modules,Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstracts.

2)基本概念

  • 依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置
  • 低层模块尽量都要有抽象类或接口,或者两者都有
  • 变量的声明类型尽量是抽象类或接口
  • 使用继承时遵循里氏替换原则
  • 设计和实现要依赖于抽象而非具体。一方面抽象化更符合人的思维习惯;另一方面,根据里氏替换原则,可以很容易将原来的抽象替换为扩展后的具体,这样可以很好的支持开-闭原则

3)优点

  • 减少类间的耦合性,提高系统的稳定性
  • 降低并行开发引起的风险
  • 提高代码的可读性和可维护性

4)示例:

public class MrZhang {
    public void study(ChineseCourse course) {
        course.content();
    }
}
public class ChineseCourse {
    public void content() {
        System.out.println("开始学习语文课程");
    }
}
public class Main {
    public static void main(String[] args) {
        MrZhang zhang = new MrZhang();
        zhang.study(new ChineseCourse());
    }
}
执行结果:
开始学习语文课程

执行之后,结果正常。那么,考虑一个问题假如此时要学习数学课程呢? 数学课程代码如下:

public class MathCourse {
    public void content() {
        System.out.println("开始学习数学课程");
    }
}

    很显然,MrZhang 无法学习,因为他只能接受 ChineseCourse ,学习语文课程。当然如果我们修改接受参数为 MathCourse 的话就可以学习了,但是不能学习语文,英语,化学等等。造成此现象的具体原因是:MrZhang 和 ChineseCourse 耦合度太高了,必须降低耦合度才可以。代码如下:

public interface ICourse {
    void content();
}
public class MrZhang {
    public void study(ICourse course) {
        course.content();
    }
}
public class ChineseCourse implements ICourse {
    @Override
    public void content() {
        System.out.println("开始学习语文课程");
    }
}
public class MathCourse implements ICourse {
    @Override
    public void content() {
        System.out.println("开始学习数学课程");
    }
}
public class Main {
    public static void main(String[] args) {
        MrZhang zhang = new MrZhang();
        zhang.study(new ChineseCourse());
        zhang.study(new MathCourse());
    }
}

MrZhang 与 ICourse 具有依赖关系,ChineseCourse 和 MathCourse 属于课程范畴,并且各自实现了 ICourse 接口,这样就符合了依赖倒置原则。这样修改后无论再怎么扩展 Main 类,都不用继续修改 MrZhang 了,MrZhang.java 作为高层模块就不会依赖低层模块的修改而引起变化,减少了修改程序造成的风险。

5.接口隔离原则 - Interface Segration Principle(ISP)

1)定义

  • 定义一:客户端不应该依赖它不需要的接口
  • Clients should not be forced to depend upon interfaces that they don’t use.
  • 定义二:类间的依赖关系应该建立在最小的接口上
  • The dependency of one class to another one should depend on the
  • smallest possible interface.

2)基本概念

  • 一个类对另一个类的依赖应该建立在最小的接口上,通俗的讲就是需要什么就提供什么,不需要的就不要提供
  • 接口中的方法应该尽量少,不要使接口过于臃肿,不要有很多不相关的逻辑方法

3)优点

  • 高内聚,低耦合
  • 可读性高,易于维护

4)代码示例:

public interface IAnimal {
    void eat();
    void talk();
    void fly();
}
public class BirdAnimal implements IAnimal {
    @Override
    public void eat() {
        System.out.println("鸟吃虫子");
    }
    @Override
    public void talk() {
        //并不是所有的鸟都会说话
    }
    @Override
    public void fly() {
        //并不是所有的鸟都会飞
    }
}
public class DogAnimal implements IAnimal {
    @Override
    public void eat() {
        System.out.println("狗狗吃饭");
    }
    @Override
    public void talk() {
        //狗不会说话
    }
    @Override
    public void fly() {
        //狗不会飞
    }
}

     通过上面的代码发现:狗实现动物接口,必须实现三个接口,根据常识我们得知,第二个和第三个接口不一定会有实际意义,换句话说也就是这个方法有可能一直不会被调用。但是就是这样我们还必须实现这个方法,尽管方法体可以为空,但是这就违反了接口隔离的定义。我们知道 由于Java类支持实现多个接口,可以很容易的让类具有多种接口的特征,同时每个类可以选择性地只实现目标接口,基于此特点我们可以对功能进一步的细化,编写一个或多个接口,代码如下:

public interface IEat {
    void eat();
}
public interface IFly {
    void fly();
}
public interface ITalk {
    void talk();
}
public class DogAnimal implements IEat {
    @Override
    public void eat() {
        System.out.println("狗狗吃饭");
    }
}
public class ParrotAnimal implements IEat, IFly, ITalk {
    @Override
    public void eat() {
        System.out.println("鹦鹉吃东西");
    }
    @Override
    public void fly() {
        System.out.println("鹦鹉吃飞翔");
    }
    @Override
    public void talk() {
        System.out.println("鹦鹉说话");
    }
}

   说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。

其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。

其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建

接口隔离原则一定要适度使用,接口设计的过大或过小都不好,过分的细粒度可能造成接口数量庞大不易于管理

6.迪米特法则/最少知道原则 - Law of Demeter or Least Knowledge Principle(LoD or LKP)

1)定义

  • 一个对象应该对其他对象保持最少的了解
  • 这个原理的名称来源于希腊神话中的农业女神,孤独的得墨忒耳。

2)基本概念

  • 每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元;
  • 每个单元只能和它的朋友交谈:不能和陌生单元交谈;
  • 只和自己直接的朋友交谈。

3)优点

  • 使得软件更好的可维护性与适应性
  • 对象较少依赖其它对象的内部结构,可以改变对象容器(container)而不用改变它的调用者(caller)

4)详细讲解:

      迪米特法则通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

代码举例:通过老师要求班长告知班级人数为例,讲解迪米特法则。先来看一下违反迪米特法则的设计,代码如下

public class Student {
    private Integer id;
    private String name;
    public Student(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
}
public class Teacher {
    public void call(Monitor monitor) {
        List<Student> sts = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            sts.add(new Student(i + 1, "name" + i));
        }
        monitor.getSize(sts);
    }
}
public class Monitor {
    public void getSize(List list) {
        System.out.println("班级人数:" + list.size());
    }
}

     现在这个设计的主要问题出在 Teacher 中,根据迪米特法则,只与直接的朋友发生通信,而 Student 类并不是 Teacher 类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲 Teacher 只与 Monitor 耦合就行了,与 Student 并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下:

public class Student {
    private Integer id;
    private String name;
    public Student(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
}
public class Teacher {
    public void call(Monitor monitor) {
        monitor.getSize();
    }
}
public class Monitor {
    public void getSize() {
        List<Student> sts = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            sts.add(new Student(i + 1, "name" + i));
        }
        System.out.println("班级人数" + sts.size());
    }
}

   将Student 从 Teacher 抽掉,也就达到了 Student 和 Teacher 的解耦,从而符合了迪米特原则。

    迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,老师(Teacher)就是通过班长(Monitor)这个“中介”来与 学生(Student)发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

7.合成/聚合复用原则 - Composite/Aggregate Reuse Principle(CARP / CRP)

1) 定义

  • 尽量采用组合(contains-a)、聚合(has-a)的方式而不是继承(is-a)的关系来达到软件的复用目的

2)基本概念

  • 如果新对象的某些功能在别的已经创建好的对象里面已经实现,那么应当尽量使用别的对象提供的功能,使之成为新对象的一部分,而不要再重新创建

组合/聚合的优缺点:类之间的耦合比较低,一个类的变化对其他类造成的影响比较少,缺点:类的数量增多实现起来比较麻烦

继承的优点:由于很多方法父类已经实现,子类的实现会相对比较简单,缺点:将父类暴露给了子类,一定程度上破坏了封装性,父类的改变对子类影响比较大

3)优点

  • 可以降低类与类之间的耦合程度
  • 提高了系统的灵活性

4)讲解

public class Person {
    public void talk(String name) {
        System.out.println(name + " say hello");
    }
    public void walk(String name) {
        System.out.println(name + " move");
    }
}
public class Manager extends Person { 
}
public class Employee extends Person {
}

   按照组合复用原则我们应该首选组合,然后才是继承,使用继承时应该严格的遵守里氏替换原则,必须满足“Is-A”的关系是才能使用继承,而组合却是一种“Has-A”的关系。导致错误的使用继承而不是使用组合的一个重要原因可能就是错误的把“Has-A”当成了“Is-A”。

   由上没看的代码可以看出,经理和员工继承了人,但实际中每个不同的职位拥有不同的角色,如果我们添加了角色这个类,那么继续使用继承的话只能使每个人只能具有一种角色,这显然是不合理的。

什么是Java的多态?

      实现多态的三个条件

  1. 继承的存在。继承是多态的基础,没有继承就没有多态
  2. 子类重写父类的方法,JVM会调用子类重写后的方法
  3. 父类引用变量指向子类对象

     向上转型:将一个父类的引用指向一个子类对象,自动进行类型转换。

  1. 通过父类引用变量调用的方法是子类覆盖或继承父类的方法,而不是父类的方法。
  2. 通过父类引用变量无法调用子类特有的方法。

      向下转型:将一个指向子类对象的引用赋给一个子类的引用,必须进行强制类型转换。

  1. 向下转型必须转换为父类引用指向的真实子类类型,不是任意的强制转换,否则会出现 ClassCastException
  2. 向下转型时可以结合使用 instanceof 运算符进行判断

6 Java中实现多态的机制是什么?

      靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

7 类和对象的关系

  1. 类是对象的抽象;对象是类的具体实例
  2. 类是抽象的,不占用内存;对象是具体的,占用存储空间
  3. 类是一个定义包括在一类对象中的方法和变量的模板

说一说你的对面向过程和面向对象的理解

  1. 软件开发思想,先有面向过程,后有面向对象
  2. 在大型软件系统中,面向过程的做法不足,从而推出了面向对象
  3. 都是解决实际问题的思维方式
  4. 两者相辅相成,宏观上面向对象把握复杂事物的关系;微观上面向过程去处理
  5. 面向过程以实现功能的函数开发为主;面向对象要首先抽象出类、属性及其方法,然后通过实例化类、执行方法来完成功能
  6. 面向过程是封装的是功能;面向对象封装的是数据和功能
  7. 面向对象具有继承性和多态性;面向过程则没有

9 抽象类必须要有抽象方法吗?

    不需要抽象类不一定非要有抽象方法

示例代码:

abstract class Cat {
    public static void sayHi() {
        System. out. println("hi~");
    }
}

上面代码,抽象类并没有抽象方法但完全可以正常运行。

10 普通类和抽象类区别?

  1. 抽象类不能被实例化
  2. 抽象类可以有抽象方法,抽象方法只需申明,无需实现
  3. 含有抽象方法的类必须申明为抽象类
  4. 抽象类的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类
  5. 抽象方法不能被声明为静态
  6. 抽象方法不能用private修饰
  7. 抽象方法不能用final修饰

11 JDK8 中为什么有接口默认方法

以前创建了一个接口,并且已经被大量的类实现。
如果需要再扩充这个接口的功能加新的方法,就会导致所有已经实现的子类需要重写这个方法。
如果在接口中使用默认方法就不会有这个问题。
所以从 JDK8 开始新加了接口默认方法,便于接口的扩展。

接口中默认方法的规则

1.默认方法使用 default 关键字,一个接口中可以有多个默认方法。
2.接口中既可以定义抽象方法,又可以定义默认方法,默认方法不是抽象方法。
3.子类实现接口的时候,可以直接调用接口中的默认方法,即继承了接口中的默认方法。
4.接口中同时还可以定义静态方法,静态方法通过接口名调用。

默认方法的案例案例需求:

  1.创建一个 Cat 接口,包含 play()抽象方法,eat()静态方法,run()默认方法,climb()默认方法

  2.写一个子类 WhiteCat 白猫实现了猫接口

  3.实现 play()抽象方法,重写 run()的默认方法

  4.创建类 Demo04Interface,在 main 函数中创建 WhiteCat 对象

       a) 调用接口中静态方法 eat()

       b) 调用白猫子类中实现的方法 play()

       c) 调用白猫子类中重写的方法 run()

       d) 调用接口中默认方法 climb()

interface Cat {
    void play(); // 抽象方法
    
    // 静态方法
    static void eat() {
        System.out.println("猫吃鱼");
    }

    // 默认方法
    default void run() {
        System.out.println("猫跑");
    }

    // 默认方法
    default void climb() {
        System.out.println("猫爬树");
    }
}

// 子类白猫实现了猫接口
class WhiteCat implements Cat {

    @Override
    public void play() { // 实现抽象方法
        System.out.println("白猫玩");
    }

    @Override
    public void run() { // 也可以重写默认方法
        System.out.println("白猫跑");
    }
}

// 主类中调用

public class Demo04Interface {

    public static void main(String[] args) {
        WhiteCat cat = new WhiteCat();
        Cat.eat(); // 调用接口中静态方法
        cat.play(); // 调用白猫子类中实现的方法
        cat.run(); // 调用白猫子类中重写的方法
        cat.climb(); // 调用接口中默认方法
    }

12 接口和抽象类区别

    1.接口的方法默认是public,所有方法在接口中不能有实现(Java8开始接口方法可以有默认实现),抽象类可以有非抽象的方法

   2.接口中的实例变量默认是final类型的,而抽象类中则不一定

   3.一个类可以实现多个接口,但最多只能实现一个抽象类

   4.一个类实现接口的话要实现接口的所有方法,而抽象类不一定

   5.接口不能用new实例化,但可以声明,但是必须引用一个实现该接口的对象从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

•       实现:抽象类的子类使用extends来继承;接口必须使用implements来实现接口。

•       构造函数:抽象类可以有构造函数;接口不能有。

•       实现数量:类可以实现很多个接口;但是只能继承一个抽象类。

•       访问修饰符:接口中的方法默认使用public修饰;抽象类中的方法可以是任意访问修饰符。

13 抽象类能使用final修饰吗?

     不能,定义抽象类就是让其他类继承的,如果定义为final该类就不能被继承,这样彼此就会产生矛盾,所以final不能修饰抽象类

14  abstract方法是否可是static的?native的?synchronized的?

都不能

  1. 抽象方法需要子类重写,而静态的方法是无法被重写的
  2. 本地方法是由本地动态库实现的方法,而抽象方法是没有实现的
  3. 抽象方法没有方法体;synchronized 方法,需要有具体的方法体,相互矛盾

15 重载(overload)和重写(override)的区别?

      重写:在子类中将父类的成员方法的名称保留,重新编写成员方法的实现内容,更改方法的访问权限,修改返回类型的为父类返回类型的子类。

一些规则:

    重写发生在子类继承父类

    参数列表必须完全与被重写方法的相同

    重写父类方法时,修改方法的权限只能从小范围到大范围

     返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的子类(JDK1.5 及更早版本返回类型要一样,JDK1.7 及更高版本可以不同)

     访问权限不能比父类中被重写的方法的访问权限更低。如:父类的方法被声明为 public,那么子类中重写该方法不能声明为 protected

     重写方法不能抛出新的检查异常和比被重写方法申明更宽泛的异常(即只能抛出父类方法抛出异常的子类)

     声明为 final 的方法不能被重写

    声明为 static 的方法不能被重写

     声明为 private 的方法不能被重写

     重载:一个类中允许同时存在一个以上的同名方法,这些方法的参数个数或者类型不同

重载条件:

  • 方法名相同
  • 参数类型不同 或 参数个数不同 或 参数顺序不同

规则:

    被重载的方法参数列表(个数或类型)不一样

    被重载的方法可以修改返回类型

    被重载的方法可以修改访问修饰符

    被重载的方法可以修改异常抛出

    方法能够在同一个类中或者在一个子类中被重载

    无法以返回值类型作为重载函数的区分标准

重载和重写的区别:

  • 作用范围:重写的作用范围是父类和子类之间;重载是发生在一个类里面
  • 参数列表:重载必须不同;重写不能修改
  • 返回类型:重载可修改;重写方法返回相同类型或子类
  • 抛出异常:重载可修改;重写可减少或删除,一定不能抛出新的或者更广的异常
  • 访问权限:重载可修改;重写一定不能做更严格的限制

16 子类构造方法的执行过程是什么样的?

子类构造方法的调用规则:

  1. 如果子类的构造方法中没有通过 super 显式调用父类的有参构造方法,也没有通过 this 显式调用自身的其他构造方法,则系统会默认先调用父类的无参构造方法。这种情况下,写不写 super(); 语句,效果是一样的
  2. 如果子类的构造方法中通过 super 显式调用父类的有参构造方法,将执行父类相应的构造方法,不执行父类无参构造方法
  3. 如果子类的构造方法中通过 this 显式调用自身的其他构造方法,将执行类中相应的构造方法
  4. 如果存在多级继承关系,在创建一个子类对象时,以上规则会多次向更高一级父类应用,一直到执行顶级父类 Object 类的无参构造方法为止

17 为什么函数不能根据返回类型来区分重载

    该道题来自华为面试题。

    因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。

例如:

float max(int a, int b);
int max(int a, int b);

    当调用 max(1, 2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。

    再比如对下面这两个方法来说,虽然它们有同样的名字和自变量,但其实是很容易区分的:

void f() {}
int f() {}

     若编译器可根据上下文(语境)明确判断出含义,比如在  int x=f()中,那么这样做完全没有问题。然而,我们也可能调用一个方法,同时忽略返回值;我们通常把这称为“为它的副作用去调用一个方法”,因为我们关心的不是返回值,而是方法调用的其他效果。所以假如我们像下面这样调用方法: f(); Java 怎样判断 f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能。

    函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”。

18 为什么Java中不支持多重继承?

      我发现这个Java核心问题很难回答,因为你的答案可能不会让面试官满意,在大多数情况下,面试官正在寻找答案中的关键点,如果你提到这些关键点,面试官会很高兴。在Java中回答这种棘手问题的关键是准备好相关主题,以应对后续的各种可能的问题。

     这是非常经典的问题,与为什么String在Java中是不可变的很类似;这两个问题之间的相似之处在于它们主要是由Java创作者的设计决策使然。

为什么Java不支持多重继承,可以考虑以下两点:

       1)第一个原因是围绕钻石形继承问题产生的歧义,考虑一个类A有foo()方法,然后B和C派生自A,并且有自己的foo()实现,现在D类使用多个继承派生自B和C,如果我们只引用foo(),编译器将无法决定它应该调用哪个foo()。这也称为Diamond问题,因为这个继承方案的结构类似于菱形,见下图:

                A foo()   

               /    

             /        

 foo() B     C foo()   

                  /   

                /   

               D  foo()

        即使我们删除钻石的顶部 A 类并允许多重继承,我们也将看到这个问题含糊性的一面。如果你把这个理由告诉面试官,他会问为什么 C++ 可以支持多重继承而 Java不行。嗯,在这种情况下,我会试着向他解释我下面给出的第二个原因,它不是因为技术难度, 而是更多的可维护和更清晰的设计是驱动因素, 虽然这只能由 Java 言语设计师确认,我们只是推测。维基百科链接有一些很好的解释,说明在使用多重继承时,由于钻石问题,不同的语言地址问题是如何产生的。

        2)对我来说第二个也是更有说服力的理由是,多重继承确实使设计复杂化并在转换、构造函数链接等过程中产生问题。假设你需要多重继承的情况并不多,简单起见,明智的决定是省略它。此外,Java 可以通过使用接口支持单继承来避免这种歧义。由于接口只有方法声明而且没有提供任何实现,因此只有一个特定方法的实现,因此不会有任何歧义。

19 为什么Java不支持运算符重载?

     另一个类似棘手的Java问题。为什么 C++ 支持运算符重载而 Java 不支持? 有人可能会说+运算符在 Java 中已被重载用于字符串连接,不要被这些论据所欺骗。

     与 C++ 不同,Java 不支持运算符重载。Java 不能为程序员提供自由的标准算术运算符重载,例如+, - ,*和/等。如果你以前用过 C++,那么 Java 与 C++ 相比少了很多功能,例如 Java 不支持多重继承,Java中没有指针,Java中没有引用传递。另一个类似的问题是关于 Java 通过引用传递,这主要表现为 Java 是通过值还是引用传参。虽然我不知道背后的真正原因,但我认为以下说法有些道理,为什么 Java 不支持运算符重载。

       1)简单性和清晰性。清晰性是Java设计者的目标之一。设计者不是只想复制语言,而是希望拥有一种清晰,真正面向对象的语言。添加运算符重载比没有它肯定会使设计更复杂,并且它可能导致更复杂的编译器, 或减慢 JVM,因为它需要做额外的工作来识别运算符的实际含义,并减少优化的机会, 以保证 Java 中运算符的行为。

       2)避免编程错误。Java 不允许用户定义的运算符重载,因为如果允许程序员进行运算符重载,将为同一运算符赋予多种含义,这将使任何开发人员的学习曲线变得陡峭,事情变得更加混乱。据观察,当语言支持运算符重载时,编程错误会增加,从而增加了开发和交付时间。由于 Java 和 JVM 已经承担了大多数开发人员的责任,如在通过提供垃圾收集器进行内存管理时,因为这个功能增加污染代码的机会, 成为编程错误之源, 因此没有多大意义。

      3)JVM复杂性。从JVM的角度来看,支持运算符重载使问题变得更加困难。通过更直观,更干净的方式使用方法重载也能实现同样的事情,因此不支持 Java 中的运算符重载是有意义的。与相对简单的 JVM 相比,复杂的 JVM 可能导致 JVM 更慢,并为保证在 Java 中运算符行为的确定性从而减少了优化代码的机会。

      4)让开发工具处理更容易。这是在 Java 中不支持运算符重载的另一个好处。省略运算符重载使语言更容易处理,这反过来又更容易开发处理语言的工具,例如 IDE 或重构工具。Java 中的重构工具远胜于 C++。

20 你能用Java覆盖静态方法吗?如果我在子类中创建相同的方法是编译时错误?

      不,你不能在Java中覆盖静态方法,但在子类中声明一个完全相同的方法不是编译时错误,这称为隐藏在Java中的方法。

    你不能覆盖Java中的静态方法,因为方法覆盖基于运行时的动态绑定,静态方法在编译时使用静态绑定进行绑定。虽然可以在子类中声明一个具有相同名称和方法签名的方法,看起来可以在 Java中覆盖静态方法,但实际上这是方法隐藏。Java不会在运行时解析方法调用,并且根据用于调用静态方法的 Object 类型,将调用相应的方法。这意味着如果你使用父类的类型来调用静态方法,那么原始静态将从父类中调用,另一方面如果你使用子类的类型来调用静态方法,则会调用来自子类的方法。简而言之,你无法在Java中覆盖静态方法。如果你使用像Eclipse或Netbeans这样的Java IDE,它们将显示警告静态方法应该使用类名而不是使用对象来调用,因为静态方法不能在Java中重写。

输出:

parent

    此输出确认你无法覆盖Java中的静态方法,并且静态方法基于类型信息而不是基于Object进行绑定。如果要覆盖静态mehtod,则会调用子类或 ColorScreen 中的方法。这一切都在讨论中我们可以覆盖Java中的静态方法。我们已经确认没有,我们不能覆盖静态方法,我们只能在Java中隐藏静态方法。创建具有相同名称和mehtod签名的静态方法称为Java隐藏方法。IDE将显示警告:"静态方法应该使用类名而不是使用对象来调用", 因为静态方法不能在Java中重写。

    这些是我的核心Java面试问题和答案的清单。对于有经验的程序员来说,一些Java问题看起来并不那么难,但对于Java中的中级和初学者来说,它们真的很难回答。

21 类的实例化方法调用顺序

类加载器实例化时进行的操作步骤:加载 -> 连接 -> 初始化

  1. 代码书写顺序加载父类静态变量和父类静态代码块
  2. 代码书写顺序加载子类静态变量和子类静态代码块
  3. 父类非静态变量(父类实例成员变量)
  4. 父类构造函数
  5. 子类非静态变量(子类实例成员变量)
  6. 子类构造函数

22 实例方法和类方法

      Java中的方法分为类方法实例方法,区别是类方法中有static修饰,为静态方法,是类的方法。所以在类文件加载到内存时就已经创建,但是实例方法是对象的方法,只有对象创建后才起作用,所以在类方法中不能调用实例方法,但实例方法中可以调用类方法,且实例方法可以互相调用

类方法不可以直接调用对象变量,但可以调用类变量

但是可以先new一个对象然后间接调用,如上图中第二个minus的使用方法

先new一个

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值