Java类结构与初始化面试题

2023/06/22

1. 概述

类结构和初始化是每个Java程序员都应该熟悉的基础知识,本文提供了你可能会遇到的关于该主题的一些面试题的答案。

Q1. 描述应用于类、方法、字段或局部变量时final关键字的含义。

当应用于不同的语言结构时,final关键字具有多种不同的含义:

  • final类是不能被子类化的类
  • final方法是不能在子类中重写的方法
  • final字段是必须在构造函数或初始化程序块中初始化并且之后不能修改的字段
  • final变量是只能被赋值(并且必须被赋值)一次并且之后永远不会被修改的变量

Q2. 什么是默认方法?

在Java 8之前,接口只能有抽象方法,即没有主体的方法。从Java 8开始,接口方法可以具有默认实现。如果实现类不重写此方法,则使用默认实现。这样的方法适当地用default关键字标记。

默认方法的一个突出用例是向现有接口添加方法。如果你不将此类接口方法标记为default,则此接口的所有现有实现都将中断。添加具有默认实现的方法可确保遗留代码与此接口的新版本的二进制兼容性。

一个很好的例子是Iterator接口,它允许一个类成为for-each循环的目标。该接口首先出现在Java 5中,但在Java 8中它包含了两个额外的方法,forEach和spliterator。它们被定义为具有实现的默认方法,因此不会破坏向后兼容性:

public interface Iterable<T> {

    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) { /* */ }

    default Spliterator<T> spliterator() { /* */ }
}

Q3. 什么是静态类成员?

类的静态字段和方法不绑定到类的特定实例;相反,它们绑定到类对象本身。调用静态方法或寻址静态字段是在编译时解决的,因为与实例方法和字段相反,我们不需要遍历引用并确定我们所引用的实际对象。

Q4. 如果一个类没有任何抽象成员,它可以被声明为抽象类吗?这样的类的目的是什么?

可以,一个类可以声明为抽象的,即使它不包含任何抽象成员。作为一个抽象类,它不能被实例化,但它可以作为某个层次结构的根对象,提供对其实现有用的方法。

Q5. 什么是构造函数链?

构造函数链是一种通过提供多个按顺序相互调用的构造函数来简化对象构造的方法。

最具体的构造函数可能采用所有可能的参数,并可能用于最详细的对象配置。不太具体的构造函数可以通过为其某些参数提供默认值来调用更具体的构造函数。在链的顶部,无参构造函数可以实例化具有默认值的对象。

下面是一个类的示例,该类模拟在一定天数内可用的百分比折扣。如果我们在使用无参构造函数时没有指定它们,则使用默认值10%和2:

public class Discount {

    private int percent;

    private int days;

    public Discount() {
        this(10);
    }

    public Discount(int percent) {
        this(percent, 2);
    }

    public Discount(int percent, int days) {
        this.percent = percent;
        this.days = days;
    }
}

Q6. 什么是方法重写和重载?他们有何不同?

当你定义具有与超类中相同签名的方法时,方法的重写是在子类中完成的,这允许运行时根据你调用该方法的实际对象类型来选择一个方法。toString、equals和hashCode方法在子类中经常被重写。

方法的重载发生在同一个类中,当你创建具有相同名称但具有不同类型或参数数量的方法时,就会发生重载。这允许你根据提供的参数类型执行特定代码,而方法名称保持不变。

下面是java.io.Writer抽象类中的重载示例。以下方法都被命名为write,但其中一个接收int而另一个接收char数组。

public abstract class Writer {

    public void write(int c) throws IOException {
        // ...
    }

    public void write(char cbuf[]) throws IOException {
        // ...
    }
}

Q7. 你能重写静态方法吗?

不能。根据定义,仅当方法的实现在运行时由实际实例的类型确定时,才能重写该方法(称为动态方法查找的过程)。静态方法的实现是在编译时使用引用的类型确定的,因此重写无论如何都没有多大意义。尽管你可以在子类中添加一个具有与超类中完全相同的签名的静态方法,但这在技术上不是重写。

Q8. 什么是不可变类,如何创建?

不可变类的实例在创建后不能更改。这里,更改的意思是通过修改实例字段的值来改变状态。不可变类有很多优点:它们是线程安全的,当你没有可变状态要考虑时,推理它们要容易得多。

要使类不可变,你应该确保以下几点:

  • 所有字段都应声明为private和final;这表明它们应该在构造函数中初始化,并且从那以后就不会改变;
  • 该类不应该有setter或其他方法来改变字段的值;
  • 通过构造函数传递的类的所有字段也应该是不可变的,或者它们的值应该在字段初始化之前被复制(否则我们可以通过保留这些值并修改它们来更改此类的状态);
  • 类的方法不应该被重写;要么所有方法都应该是final,要么构造函数应该是私有的并且只能通过静态工厂方法调用。

Q9. 如何比较两个枚举值:使用equals()还是使用==?

实际上,你可以同时使用两者。枚举值是对象,因此可以使用equals()将它们进行比较,但它们在底层也是作为静态常量实现的,因此你也可以使用==将它们进行比较。这主要是代码风格的问题,但如果你想节省字符空间(并可能跳过不需要的方法调用),你应该使用==进行比较。

Q10. 什么是初始化程序块?什么是静态初始化程序块?

初始化程序块是类范围内的花括号代码块,在实例创建期间执行,你可以使用它来初始化比就地初始化单行更复杂的字段。

实际上,编译器只是在每个构造函数中复制这个块,因此这是从所有构造函数中提取公共代码的好方法。

静态初始化程序块也是花括号代码块,其前面带有static修饰符。它在类加载期间执行一次,可用于初始化静态字段或用于某些副作用。

Q11. 什么是标记接口?Java中标记接口的著名示例有哪些?

标记接口是没有任何方法的接口,它通常由类实现或由另一个接口扩展以表示某种属性。标准Java库中最广为人知的标记接口如下:

  • Serializable用于明确表示该类可以被序列化
  • Cloneable允许使用clone方法克隆对象(没有Cloneable接口,此方法抛出CloneNotSupportedException)
  • Remote在RMI中用于指定可以远程调用方法的接口

Q12. 什么是单例以及如何在Java中实现它?

单例是面向对象编程的一种模式,一个单例类可能只有一个实例,通常是全局可见和可访问的。

在Java中有多种创建单例的方法,以下是最简单的示例,其中包含就地初始化的静态字段。初始化是线程安全的,因为静态字段保证以线程安全的方式初始化。构造函数是私有的,因此外部代码无法创建该类的多个实例。

public class SingletonExample {

    private static SingletonExample instance = new SingletonExample();

    private SingletonExample() {
    }

    public static SingletonExample getInstance() {
        return instance;
    }
}

但这种方法可能有一个严重的缺点-实例将在首次访问此类时被实例化,如果此类的初始化是一项繁重的操作,我们可能希望将其推迟到实际需要实例时(可能永远不需要),但同时保持线程安全。在这种情况下,我们应该使用一种称为双重检查锁的技术。

Q13. 什么是可变参数?可变参数有哪些限制?如何在方法体内使用它?

可变参数是方法的可变长度参数。一个方法只能有一个可变参数,并且它必须在参数列表中排在最后。它被指定为一个类型名称,后跟一个省略号和一个参数名称。在方法主体内部,可变参数用作指定类型的数组。

下面是标准库中的一个示例-Collections.addAll方法,它接收一个集合、可变数量的元素,并将所有元素添加到集合中:

public static <T> boolean addAll(Collection<? super T> c, T... elements) {
    boolean result = false;
    for (T element : elements)
        result |= c.add(element);
    return result;
}

Q14. 你可以访问超类的重写方法吗?你能以类似的方式访问超超类的重写方法吗?

要访问超类的重写方法,可以使用super关键字。但是你没有类似的方法来访问超超类的重写方法。

作为标准库中的一个示例,LinkedHashMap类扩展了HashMap并且大部分都重用了它的功能,在其值上添加了一个链表以保持迭代顺序。LinkedHashMap重用其超类的clear方法,然后清除其链表的头尾引用:

public void clear() {
    super.clear();
    head = tail = null;
}
Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章