跳至主要內容

java基础高频面试题1

fangzhipeng约 4743 字大约 16 分钟

重载与重写的区别

重载(Overloading)和重写(Overriding)是Java语言中的两个重要概念,它们都是用于类的方法。正是由于重载和重写,类的多态性得到了充分的展现。

重载(Overloading):

重载指的是在一个类中定义多个方法,这些方法具有相同的方法名称,但是参数的个数不同,或者参数的类型不同,或者是参数的顺序不同。重载的方法通过调用时的参数的类型或者数量、顺序来区分调用了哪个方法。重载的方法的返回结果的类型也可以是不同的,但是不能返回结果的类型来定义方法的重载。

重载有以下的特点:

  • 方法名称相同,参数的类型可以不同、参数的个数也可以不同、参数的顺序也可以不同
  • 不同的重载方法可以返回不同的结果;但是不同的返回结果不能作为方法重载的定义。
  • 编译是通过方法的不同的参数来确定方法

代码示例:

public class OverLodingDemo {
    public static void main(String[] args) {
        OverLodingDemo demo=new OverLodingDemo();
        demo.greeting("lisi");
        demo.greeting("lisi","wangwu");
    }

    public void greeting(String name){
        System.out.println("hello "+name);
    }
    public void greeting(String name1,String name2){
        System.out.println("hello "+name1 );
        System.out.println("hello "+name2 );
    }
}

在上面的例子中,有两个greeting方法,这两个greeting方法有不同的参数个数,属于方法的重载。

重写(Overriding):

重写是指子类对父类方法的重写,它是子类对父类的同名方法且具有相同的参数的重新实现。子类通过重写父类的方法来修改或者扩展父类方法的行为。

被重写的方法具有以下的特点:

  • 子类的方法名称和父类完全相同,且具有相同的参数和返回结构类型
  • 重新的方法必须具有相同的访问修饰词或者更宽松的访问级别。
  • 它是动态绑定来确定运行时对象的实际方法的调用。

举个简单的例子,有Animal的父类,有一个eat的方法:

public class Animal {

    public void eat(){
        System.out.println("animal eat");
    }
}

Sheep继承了Animal父类,并重写了eat方法:

public class Sheep extends Animal{
    @Override
    public void eat(){
        System.out.println("sheep eat grass");
    }
}

Dog继承了Animal父类,没有重写了eat方法:

public class Dog extends Animal{
}

写一个测试类:

public class OverwrittingDemo {

    public static void main(String[] args) {
        Animal sheep=new Sheep();
        sheep.eat();
        Animal dog=new Dog();
        dog.eat();
    }
}

执行结果如下:

sheep eat grass
animal eat

总结:

  • 重载是针对于同个类中,重新是发生在子类和父类的继承关系之间。
  • 重载是根据方法的参数列表进行区分,重写是根据方法的名称和参数列表进行区分。
  • 重载方法在编译时静态地绑定,而重写方法在运行时动态地绑定。
  • 重载方法可以有不同的返回类型,而重写方法必须具有相同的返回类型。

抽象类和接口的区别

抽象类

抽象类通过使用关键字 "abstract" 来声明,它不能被实例化,只能用作基类或父类来使用。抽象类用于定义通用的属性和行为,可以包含抽象方法、普通方法、字段以及构造方法。抽象方法只有方法签名,没有方法体。抽象方法必须在非抽象子类中被重写实现。

抽象类通常为其子类提供一个通用的模板,子类必须实现抽象类中的抽象方法。

以下是一个抽象类的示例:

public abstract class Shape {
    private String color;

    public Shape(String color) {
        this.color = color;
    }

    abstract double getArea();
}

public class Circle extends Shape{
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

public class Demo {

    public static void main(String[] args) {
        Shape circle=new Circle("blue",23);
        System.out.println(circle.getArea());
    }
}

在上面的这个例子中,抽象类 Shape 声明了抽象方法 getArea() 。Circle 是其子类,并实现了抽象方法。需要注意的是虽然抽象类Shape具有构造函数,但是它是不能给被实例化的。可以通过抽象类的引用变量来指向它的实现类的实例。在上述示例中,可以使用 Shape 类型的引用变量 引用了 Circle 的实例对象。

接口

接口使用interface关键字定义的,它定义了一组方法的方法,通常是抽象方法,这些方法只有方法签名,没有方法体。这些抽象方法通常是定义了一组规范或者行为,但是没有实现。

接口可以被类实现,一个类可以实现多个接口,而且同一个接口可以被多个类实现,从而使得一个实现类有不同接口的多态性。

在接口定义中,方法默认是抽象类并且是pulic方法,所以在定义接口方法时不需要添加关键字 public和abstract。接口定义中可以包含常量、默认方法(Java 8 及以上版本)、静态方法(Java 8 及以上版本)和私有方法(Java 9 及以上版本)。

以下是一个接口的示例:



interface Animal {
    int LEGS = 4; // 常量

    void makeSound(); // 抽象方法

    default void sleep() { // 默认方法
        System.out.println("Animal is sleeping.");
    }

    static void eat() { // 静态方法
        System.out.println("Animal is eating.");
    }

    private void run() { // 私有方法
        System.out.println("Animal is running.");
    }
}

public class Dog implements Animal{
    @Override
    public void makeSound() {
        System.out.println("dog bark");
    }
}


public class AnimalDemo {

    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.makeSound();
        dog.sleep();
        Animal.eat();
    }

}

在上面的例子中,

接口Animal,定义了常量 LEGS 和抽象方法 makeSound(),以及默认方法 sleep() 和静态方法 eat()。Dog 类实现了 Animal 接口,实现了 makeSound() 方法。

注意几点:

  • 类通过使用关键字 "implements" 来实现接口。一个类可以实现多个接口
  • 接口中的常量默认是静态常量。默认被 public static final 关键字修饰,可以直接通过接口名直接访问。
  • 接口的默认方法可以被实现该接口的类直接使用,实现类也可以重写接口的默认方法。
  • 静态方法在接口中提供了一些与接口相关的工具方法,可以直接通过接口名调用。
  • 私有方法在接口中定义一些辅助方法,只能在接口内部被调用,不能被实现类调用。

接口的优点在于它提供了一种灵活的方式来定义类之间的契约和行为。通过实现接口,我们可以实现代码的解耦和类之间的松耦合。接口还为多重继承提供了解决方案,使得一个类可以具备多种类型的行为。

抽象类VS接口

抽象类和接口是Java中用于实现抽象和多态性的两个关键概念。它们有一些共同点,但也有一些区别。

区别如下:

  1. 实现方式:

    • 抽象类:通过关键字 abstract 定义的类,可以包含普通方法和抽象方法,可以有实例变量和构造方法。可以被继承,用于构建类的继承层级结构。
    • 接口:通过关键字 interface 定义的,只能包含常量和抽象方法(Java 8 之后,接口也可以包含默认方法和静态方法)。没有实例变量和构造方法。可以被类实现,用于实现类的多态性。
  2. 多继承的支持:

    • 抽象类:Java只支持单继承,一个类只能继承一个抽象类。
    • 接口:Java支持多实现,一个类可以实现多个接口。
  3. 构造方法:

    • 抽象类:可以有构造方法,用于实例化抽象类的对象。
    • 接口:不能有构造方法,接口只提供方法的声明,不能实例化对象。
  4. 方法实现:

    • 抽象类:可以包含普通方法的实现。子类可以选择性地重写或调用父类的普通方法。
    • 接口:只能包含抽象方法的声明,不包含方法的实现。实现接口的类必须提供所有抽象方法的具体实现。
  5. 设计目的:

    • 抽象类:用于描述一种通用的概念,如动物类、车类等,提供一些通用方法的实现,用作其他类的基类。
    • 接口:用于定义一组合同类型的操作,描述了一个类应该具备的行为,提供了一种通用的规范,用于实现类的多态性。

需要注意的是,如果一个类既需要作为其他类的基类,又需要进行多种类型的实现,那么在设计时可以考虑将类设计为抽象类,并实现相关接口。这样可以兼顾抽象类和接口的优势。

为什么Java不支持多继承

多继承是指一个类可以从多个父类继承属性和方法。尽管多继承在某些情况下可能很有用,但它也带来了一些问题。

  1. 命名冲突:当一个类从多个父类继承相同名称的属性或方法时,可能会产生命名冲突。解决这些冲突需要额外的语法规则和解析过程,增加了语言的复杂性和理解上的困惑。

  2. 菱形继承问题:多继承可能导致菱形继承问题,即当一个类同时继承自两个具有共同父类的类时,可能在继承链中得到两个相同的父类实例,导致不确定性。

Java不支持多继承是为了避免多继承带来的上述问题。

Java选择了单继承并使用了接口(interface)来解决多继承的需要。一个类可以实现多个接口,从而达到类似多继承的效果。使用接口可以避免命名冲突和菱形继承问题,并且提供了更灵活的类组合方式。

在Java中,接口的设计可以更好地支持代码的组织和抽象,同时提供了更好的可维护性和扩展性。Java的设计理念是"Prefer composition over inheritance"(倾向于组合而非继承),鼓励使用组合和接口实现代码的复用和扩展。这种设计思想能够提高代码的可读性、可维护性和可扩展性。

String和StringBuffer、StringBuilder的区别是什么

在Java中,String、StringBuffer和StringBuilder是用于处理字符串的类,它们之间有以下区别:

  1. 可变性:
    • String是不可变(Immutable)的类,一旦创建就不能被修改。每次对String进行修改操作都会创建一个新的String对象,原来的String对象不会被改变。
    • StringBuffer和StringBuilder是可变(Mutable)的类,可以在原有对象的基础上进行修改。可以对其进行修改、删除、替换等操作,而不会创建新的对象。
  2. 线程安全性:
    • String是线程安全的,可以被多个线程同时访问而不会导致错误。
    • StringBuffer是线程安全的,它的方法都使用了synchronized关键字来进行同步,保证了线程安全。
    • StringBuilder是非线程安全的,在单线程环境中,StringBuilder的性能比StringBuffer性能更好。
  3. 性能:
    • 由于String是不可变的,每次对String进行修改时都要创建一个新的对象,当需要频繁修改字符串时会产生大量的垃圾对象,对性能会有一定的影响。
    • StringBuffer和StringBuilder是可变的,对字符串进行操作时不需要创建新的对象,因此在频繁修改字符串的场景下,性能较好。

使用场景:

  • 当字符串不需要修改时,可以使用String。

  • 当需要对字符串进行频繁的修改时,建议使用StringBuffer或StringBuilder。

    • 如果在多线程环境下执行字符串操作,建议使用StringBuffer来保证线程安全。
    • 如果在单线程环境下进行字符串操作,可以使用StringBuilder来获得更好的性能。

如何理解面向过程和面向对象

面向过程(Procedural Programming)和面向对象(Object-Oriented Programming)是两种不同的编程范式。

面向过程是一种基于步骤和函数的编程方法。在面向过程编程中,程序被分解为可重用的功能模块,每个模块都包含一系列的操作步骤。程序按照一定的顺序执行这些步骤来完成任务。面向过程的关注点主要在于解决问题的步骤和流程。

相比之下,面向对象是一种基于对象和类的编程方法。在面向对象编程中,程序由相互作用的对象组成,每个对象都是某个类的实例。每个对象都有自己的状态(属性)和行为(方法)。对象之间通过消息传递来进行通信和协作。面向对象的关注点主要在于构建对象的结构和定义对象的行为。

面向过程和面向对象都有各自的优点和适用场景。面向过程更适合简单的任务和算法,它更直观、易于理解和调试。面向对象更适合复杂的应用,它具有更好的可维护性、可扩展性和重用性。

在实际编程中,可以根据问题的性质和需求选择使用面向过程或面向对象的编程方法,或者结合两者的特点使用混合编程。

Java基本类型为什么需要包装类

Java基本类型(比如int、char、float、double等)需要包装类的原因有以下几个方面:

  1. 泛型支持:Java的泛型不支持基本类型,只能使用对象类型。所以如果想要在泛型中使用一个基本类型的值,就需要将其包装成相应的对象类型。

  2. null赋值: 基本类型的取值范围是有限的,不能表示空值或null。使用包装类可以使得变量可以被赋值为null,方便更好地表示一个变量无值的状态。

  3. 面向对象:Java是一种面向对象的语言,基本类型不是对象。如果使用基本类型,就无法访问其对象方法和属性。使用包装类可以让基本类型像一个对象一样使用。

  4. 方法重载:Java不允许在方法中使用同名的不同类型的参数。因为基本类型的参数值之间无法进行隐式转换,所以需要使用包装类来为这些基本类型提供一个相应的对象类型,以便方法可以在这些对象类型之间进行选择。

  5. 其他一些功能:包装类还提供了许多其他功能,比如数学运算、类型转换和字符串解析等。

Java提供了对应的包装类来对基本类型进行包装,例如Integer、Character、Float、Double等等。这些包装类实现了相应类型的对象类型,提供了访问基本类型数据的方法。

对于包装类,还有一些需要注意的点:

  1. 包装类都是不可变的,一旦创建,其值就不能被修改。
  2. 包装类的值比较应该使用equals()方法而不是==,因为包装类对象在使用==比较时,比较的是引用而不是值,这可能导致意外的结果。
  3. 在自动装箱和拆箱时,Java会自动调用相应的包装类方法。

例如,以下代码演示了使用包装类实现基本类型的自动装箱和拆箱:

Integer i = 10;  // 自动装箱
int j = i;       // 自动拆箱

在这个例子中,编译器会自动将10包装成一个Integer对象,并将其赋值给i。在下一行,编译器又将i自动拆箱成int值,然后赋值给j。

如何理解java中的多态

在 Java 中,多态是面向对象编程的一个核心概念,它允许我们使用父类的引用变量来引用子类的对象实例。多态主要体现在方法的重写和方法的动态绑定上。

具体来说,可以通过以下几个方面来理解 Java 中的多态:

  1. 父类引用指向子类对象实例。这样做的好处是可以通过统一的接口来操作不同的子类对象,实现了代码的灵活性和扩展性。

  2. 方法的重写:子类可以重写(覆盖)从父类继承过来的方法。当使用父类引用指向子类对象时,通过父类引用调用重写的方法时,实际上会调用子类中的方法,这就实现了运行时的动态绑定。

  3. 动态绑定:Java 的方法调用是基于运行时类型而不是编译时类型。也就是说,当调用一个对象的方法时,实际调用的是该对象的实际类型中定义的方法。这种动态绑定机制使得程序可以以统一的方式处理不同的子类对象,实现了多态性的特性。

多态性是继承的一个重要应用,它让我们可以使用父类的引用变量来引用不同子类的对象,并根据实际对象的类型来执行对应的方法。这样做的好处是实现了代码的灵活性、可扩展性和易维护性。

例如,假设有一个抽象类 Animal 和它的两个子类 Cat 和 Dog,它们都有一个共同的方法 makeSound():

abstract class Animal {
    public abstract void makeSound();
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

现在我们可以通过 Animal 类的引用变量来引用 Cat 或 Dog 对象,并根据实际对象的类型来调用 makeSound() 方法:

Animal animal1 = new Cat();
Animal animal2 = new Dog();

animal1.makeSound();  // 输出 "Meow!"
animal2.makeSound();  // 输出 "Woof!"

在这个例子中,我们使用父类 Animal 的引用变量 animal1 和 animal2 分别引用了 Cat 和 Dog 对象。当调用 makeSound() 方法时,由于动态绑定的机制,实际上调用的是 Cat 和 Dog 对象中重写的 makeSound() 方法。

需要注意的是在使用父类引用的变量去调用方法时,只能调用父类中声明的方法和子类中重写了父类的方法。要调用子类特有的方法,需要将父类引用的变量进行转换成子类的类型。

方志朋_官方公众号
方志朋_官方公众号