深度思考java基础面经

基础

1 重载和重写的区别

1.1 区别

重载和重写是两个编程术语,主要用在面向对象的编程语言中。以下是重载和重写的主要区别:
方法重载(Overloading):发生在同一个类中,方法重载是指一个类中定义了多个同名方法,但这些方法的参数列表不同(可以是参数数量、参数类型或者参数顺序不同)。这样,即使方法的名字相同,但由于参数列表不同,所以这些方法也被视为不同的方法。以下是Java语言的一个例子:

class Demo {
  void func() { ... }
  void func(int a) { ... }
  void func(int a, int b) { ... }
}

在上述例子中,func方法被重载了三次。
方法重写(Overriding):发生在子类和父类之间,当子类需要改变父类方法的行为时,可以定义一个与父类方法签名完全相同的方法,这就是方法的重写。重写的方法在调用时,会调用子类的方法,而不是父类的方法。以下是Java语言的一个例子:

class Parent {
  void func() { ... }
}

class Child extends Parent {
  @Override
  void func() { ... }
}

在上述例子中,Child类重写了Parent类的func方法。
总结一下,方法重载是同一个类中多个方法拥有相同的名字,但是参数列表不同;方法重写是子类定义了一个与父类方法签名完全相同的方法。

1.2 那一个类中的两个方法只有返回值不同其他都相同算重载吗?为什么

不算,因为标识一个类中的唯一一个方法是用到了方法名和参数列表的

在大多数面向对象的编程语言(如Java,C#等)中,一个类的两个方法如果只有返回值类型不同,参数列表相同,这种情况不能被认定为重载。方法的重载是根据方法名和参数列表(包括参数的类型、数量和顺序)来决定的,而不包括返回值类型。返回值类型不能用来区分重载方法。
例如在Java中,以下的代码是无效的:

class MyClass {
    int foo() {
        return 1;
    }

    String foo() {
        return "Hello";
    }
}

编译器会报错,因为这两个foo方法在参数列表上并没有任何区别,仅仅是返回值类型不同,无法实现重载。
所以回答你的问题,一个类中的两个方法只有返回值不同,其他相同(包括方法名和参数列表),这种情况不能算作是方法的重载。

2 final关键字

2.1 final可以用来修饰什么(final修饰类和方法没有答好)

在Java中,final关键字可以用来修饰类、方法和变量。
修饰类:当final用来修饰一个类时,表明这个类不能被继承。也就是说,没有其他类可以继承这个
final类。例如,Java中的String类就是一个final类。

final class MyClass {
    // ...
}

修饰方法:当final用来修饰一个方法时,表明这个方法不能被子类重写(Override)。但是,它并不影响方法的重载(Overload)。

class MyClass {
    final void myMethod() {
        // ...
    }
}

修饰变量:当final用来修饰一个变量时,表明这个变量的值一旦被初始化后,就不能再被改变。这对于基本类型来说意味着其数值不能改变,对于引用类型来说意味着其引用不能改变,但引用指向的对象的内容是可以改变的。如果final变量是类的成员变量,那么必须确保在构造器退出时,这些
final变量已经被初始化。

final int myVariable = 10;
final MyClass myObject = new MyClass();

以上是Java语言中final关键字的主要用法。在其他一些语言中,如C++,也有类似的关键字实现类似的功能。

2.2 为什么需要final关键字

final关键字在Java中有三个基本的用途:

  • 创建常量:对于一个值在初始化后不应改变的字段,我们可以使用final关键字。它保证了该字段的值一旦被赋值后,就不能被重新赋值。

  • 防止方法被重写:如果你想要某个方法在子类中保持不变,不被重写,你可以将它声明为final方法。

  • 防止类被继承:如果你不希望其他类继才你的类,你可以将你的类声明为final类。

使用final关键字可以提高代码的安全性和可读性。它为你提供了一种机制,可以明确地表示某个值、方法或类在设计中是不应被修改的。

2.3 为什么有的类,方法,变量需要被final修饰

类、方法或变量被声明为final,通常是因为它们在设计上不应被改变。以下是一些具体的原因:

  • 设计不变性:如果你有一个值,它在初始化后应该保持不变,你可以将它声明为final字段。例如,如果你有一个表示数学中的π的字段,你应该将它声明为final,因为π的值是不变的。

  • 防止行为的变化:如果你有一个方法,你不希望它在子类中被重写,你可以将它声明为final方法。这通常用于某些关键的、安全敏感的方法,你希望保证它们的行为不被改变。

  • 防止类的继承:如果你有一个类,你不希望其他类继承它,你可以将它声明为final类。这通常用于一些不应被扩展的类,例如某些工具类或者某些表示具体事物的类。在Java标准库中,许多表示原始类型的包装类,如IntegerLong,都被声明为final类。

另外,在Java 8中引入的Lambda表达式中,被Lambda表达式访问的局部变量必须是final或者事实上的final,也就是说,这个局部变量在初始化后就没有被再次赋值。这个限制是为了防止Lambda表达式中的并发问题。

3 StringBuffer,StringBuilder(没答好)

3.1 区别

StringBuffer和StringBuilder都是用于处理和操作字符串的类,它们在API和功能上几乎是一样的,主要区别在于同步性(synchronization):

  • StringBuffer是线程安全的,它的大部分主要方法,如append(),insert(),delete(),reverse()等都是synchronized的,这意味着在多线程环境下,只有一个线程可以访问这些方法。如果你的程序需要大量的线程安全的字符串操作,那么应该使用StringBuffer。

  • StringBuilder不是线程安全的,它的主要方法没有synchronized关键字修饰。如果你的程序主要是单线程,或者对同步没有要求,那么使用StringBuilder会比StringBuffer更快,因为它避免了同步带来的开销。
    这两个类都是为了解决字符串操作的性能问题。因为String类是不可变的,每次修改都会生成一个新的字符串对象,如果有大量的修改操作,那么这将会导致大量的内存开销和性能问题。而StringBuffer和StringBuilder都是可变的,可以在原对象上进行修改操作,因此更高效。

3.2 为什么已经有一个线程安全的StringBuffer,还要有StringBuilder呢

总的来说,你应该根据你的程序是否需要线程安全来选择使用StringBuffer或者StringBuilder。在大多数情况下,StringBuilder可能是更好的选择,因为它的性能更高,而且大多数情况下字符串操作不需要线程安全。

3.2.1 这里的更高性能是相对来说加锁取锁的操作对吧,因为这涉及到用户到内核态的转换对吗?

是的,你理解得很正确。StringBuilder的性能优于StringBuffer主要是因为它避免了synchronized带来的开销

当我们谈论线程同步时,我们经常会提到加锁和释放锁的开销。这些开销包括多个方面:

  • 互斥(Mutex):如果有多个线程试图获取同一把锁,那么除了一个线程能获取到锁,其他线程都会被阻塞。这会导致线程调度的开销,并可能导致线程的上下文切换。
  • 用户态到内核态的转换:在Java中,synchronized关键字实现的锁是一种重量级锁,这意味着它需要操作系统的支持。当一个线程试图获取一个已经被其他线程持有的锁时,它会被挂起,这涉及到用户态到内核态的转换,这是一种非常耗时的操作。.

所以,在不需要线程安全的情况下,使用StringBuilder能避免这些开销,因此性能会更好。但是请注意,如果你的代码需要在多线程环境下运行,并且需要进行字符串操作,那么你应该使用StringBuffer来保证线程安全。

3.3 何时用StringBuffer,何时用StringBuilder

  • StringBuilderStringBuffer的功能基本相同,都是用于处理字符串的可变类。两者最主要的区别在于,StringBuffer是线程安全的,而StringBuilder不是。

  • 因此,如果你的代码运行在多线程环境,并且可能有多个线程同时操作字符串,你应该使用StringBuffer

  • 如果你的代码只在单线程环境运行,或者确保同一时间只有一个线程操作字符串,那么你应该使用StringBuilder,因为它的性能比StringBuffer更好。

3.4 所有字符串拼接的时候都用StringBuilder对吗

  • 对于简单的字符串拼接,如 String s = "Hello" + " World",实际上Java编译器会自动将其转换为StringBuilder的操作。所以在这种情况下,你直接使用+操作符就可以,无需显式地使用StringBuilder

  • 对于在循环或者条件语句中的字符串拼接,应该使用StringBuilderStringBuffer,以提高性能。例如:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.append(i);
}
String s = sb.toString();
  • 如果你是在多线程环境下进行字符串拼接,你应该使用StringBuffer,以保证线程安全。

  • 如果你在处理非常大的字符串,或者进行大量的字符串操作,使用StringBuilderStringBuffer可以显著提高性能。因为String是不可变的,每次拼接都会生成一个新的字符串对象,这会产生大量的临时对象,消耗更多的内存,并增加垃圾收集的负担。而StringBuilderStringBuffer是可变的,拼接操作只是在原有的对象上进行,不会生成新的对象。

4 HashMap和concurrentHashMap

HashMap面经补充

5 接口和抽象类

5.1 抽象类和接口的区别

  • 定义:抽象类是具有一个或多个抽象方法(即没有具体实现的方法)的类,而接口是一种完全抽象的类型,它的所有方法都是抽象的。

  • 实现和继承:一个类可以实现多个接口,但只能继承一个抽象类。如果一个类继承了某个抽象类,则它必须实现抽象类中的所有抽象方法,或者自己也声明为抽象类。如果一个类实现了某个接口,则它必须实现接口中的所有方法。

  • 成员:抽象类可以包含非抽象的方法(即具有实现的方法),字段,以及私有方法。而接口只能包含抽象方法和常量,不能包含普通字段和私有方法(在 Java 8 之后,接口可以包含默认方法和静态方法)。

5.2 为什么有抽象类了还要接口,为什么有继承了还要实现类

  • 抽象类和接口:抽象类和接口的设计目的不完全相同。抽象类主要是用来实现代码复用和扩展,它可以定义一些默认的方法实现。而接口主要是用来定义类型的行为,它只定义了一组方法签名,没有提供任何实现(虽然 Java 8 之后的接口可以包含默认方法)。如果一个类需要实现某种行为,但这种行为不能通过“是一个”的关系来描述,那么应该使用接口,而不是抽象类。例如,一个类可能既是一个可飞行的对象,也是一个可移动的对象,这就需要定义两个接口,分别代表可飞行的行为和可移动的行为。

  • 继承和实现类:继承和实现类的机制也是不同的。继承代表了一种“是一个”的关系,而实现类代表了一种“具有某种行为”的关系。例如,我们可以说“猫”是一个“动物”,这是一种继承关系;而“猫”具有“走路”的行为,这是一种实现类的关系。通过继承,我们可以复用和扩展父类的代码;通过实现类,我们可以定义和实现特定的行为。

5.3 接口和抽象类可以分别帮助实现哪些设计模式

  • 接口:接口的使用可以帮助实现很多设计模式,其中包括策略模式(定义一组算法,把它们分别封装起来,让它们可以相互替换)、观察者模式(一个目标物件管理所有依赖于它的观察者物件,并且在本身的状态改变时主动发出通知)、适配器模式(把一个类的接口变换成客户端所期待的另一种接口,使原本因接口不匹配不能一起工作的两个类能够一起工作)等。

  • 抽象类:抽象类的使用可以帮助实现模板方法模式(在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行)和工厂方法模式(定义一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法模式让类把实例化推迟到子类)等。

5.4 为什么要定义一组行为?

定义一组行为(也就是一组方法)是一种编程技巧,叫做面向接口编程或者编程到接口。这种技巧的目的是降低程序的耦合度,提高程序的可扩展性。通过定义接口,我们可以让多个类实现同一个接口,然后在程序中使用接口类型的引用变量来引用这些对象。这样,我们就可以在不知道具体类的情况下,调用这些对象的方法,实现了程序的动态扩展。

5.5 用普通类不能代替抽象类吗?

抽象类和普通类在某些方面是相似的,因为它们都可以包含字段和方法。但是,抽象类和普通类在以下几个方面是不同的:

  • 抽象类不能被实例化。你不能创建一个抽象类的对象,你只能创建它的子类的对象。

  • 抽象类可以包含抽象方法,而普通类不能。抽象方法是一种只有声明没有实现的方法。子类必须提供这些抽象方法的实现,或者它自己也必须声明为抽象类。

所以,你不能用普通类来代替抽象类,除非你不需要抽象类的这些特性。

7 OOP的六大实现原则

7.1 OOP的六大实现原则:

以下六个原则都是试图降低系统复杂度,提高代码的可读性、可维护性和可重用性,以及系统的可扩展性和灵活性的。

  • 单一职责原则:一个类只应该有一个引起变化的原因。该原则告诉我们一个类应该只负责一项职责。

  • 开闭原则:软件实体(类、模块、函数等)应该可以扩展,但是不可修改。也就是说,对于已经存在的代码,我们应该尽量通过扩展的方式进行改变,而不是修改原有的代码。

  • 里氏替换原则:子类型必须能够替换它们的父类型。这个原则主要涉及到继承复用的合理性问题,即在使用基类的地方,可以透明的使用其子类。

  • 依赖倒置原则:依赖于抽象而不是一个实例。也就是要求对抽象进行编程,不要对实现进行编程。

  • 接口隔离原则:客户端不应该依赖它不需要的接口。如果一个接口在实现时,部分方法由于不需要而未被使用,那么应该将这些方法分离出去,再进行实现,避免造成冗余。

  • 合成复用原则:尽量使用对象组合,而不是继承来达到复用的目的。

7.2 java里面的哪些设计体现了OOP的这些原则

  1. 单一职责原则:Java中的每一个类都应该只有一个职责。例如,一个Person类可以负责保存和操作人的相关信息,比如姓名、年龄、性别等;而Database类可以负责与数据库的所有交互,包括存储、检索和更新数据。

  2. 开闭原则:Java中接口和抽象类的设计就是体现了开闭原则。例如,一个Sortable接口可以定义一个排序的方法,然后我们可以创建多个实现这个接口的类,比如QuickSortMergeSort等,而不需要修改Sortable接口的代码。

  3. 里氏替换原则:Java的多态性就是体现了里氏替换原则。我们可以定义一个父类的引用来指向一个子类的对象。例如,如果DogAnimal的子类,那么我们可以这样做:

    Animal animal = new Dog();
    
  4. 依赖倒置原则:Java中的依赖注入就是体现了依赖倒置原则。比如,在Spring框架中,我们可以通过注入的方式将低层的类传递给高层的类,而不是在高层的类中直接创建低层的对象。

  5. 接口隔离原则:Java中的接口设计就是体现了接口隔离原则。例如,我们可以定义一个Readable接口和一个Writable接口,而不是定义一个包含读和写两种方法的接口。这样,对于只需要读功能的类,我们就可以只实现Readable接口,而不需要实现Writable接口。

  6. 合成复用原则:Java的继承和组合都可以用来复用代码,但是优先使用组合而不是继承可以更好地体现合成复用原则。例如,如果一个类需要使用队列的功能,我们可以在这个类中包含一个队列对象,而不是让这个类继承队列类。

你可能感兴趣的:(java,java基础面经)