Java核心面试问题 之 第2部分

Java面试问题系列:第1部分中,我们讨论了面试官通常问的一些重要问题。现在是推进该讨论的时候了。在这篇文章中,我将在下面给出的问题列表中进行讨论。

为什么要避免使用finalize()方法?
为什么不应该在多线程环境中使用HashMap?它也会引起无限循环吗?
解释抽象和封装?它们有什么关系?
接口和抽象类之间的区别?
StringBuffer如何节省内存?
为什么在对象类而不是线程中声明了wait and notify?
编写Java程序以在Java中创建死锁并修复死锁?
如果您的Serializable类包含一个不可序列化的成员,该怎么办?您如何解决?
解释Java中的瞬态和volatile关键字?
Iterator和ListIterator之间的区别?

为什么要避免使用finalize()方法?

我们都知道finalize()在回收分配给对象的内存之前,垃圾回收器线程会调用方法的基本声明。请参阅此程序该程序证明finalize()根本不能保证调用。其他原因可能是:

  1. finalize()方法不能像构造函数那样在链接中工作。这意味着就像您调用构造函数时一样,所有超类的构造函数都将被隐式调用。但是,在使用finalize方法的情况下,则不遵循此方法。超类的finalize()应该显式调用。
  2. 由finalize方法引发的任何异常都将被GC线程忽略,并且不会进一步传播,实际上不会记录在日志文件中。真糟糕,不是吗?
  3. 另外,当您的类中包含finalize()时,也会影响性能。Joshua bloch在有效的Java(第2版)中说,“哦,还有一件事:使用终结器会严重影响性能。在我的机器上,创建和销毁简单对象的时间约为5.6 ns。添加终结器会将时间增加到2,400 ns。换句话说,使用终结器创建和销毁对象要慢430倍。”

为什么不应该在多线程环境中使用HashMap?它也会引起无限循环吗?

我们知道这HashMap是非同步集合,因为它的同步对象是HashTable。因此,当您在多线程环境中访问集合并且所有线程都在访问集合的单个实例时,HashTable出于各种显而易见的原因(例如避免脏读和保持数据一致性),使用它的安全性更高。在最坏的情况下,这种多线程环境也可能导致无限循环。

是的,它是真的。HashMap.get()可能导致无限循环。让我们看看如何?

如果您查看源代码HashMap.get(Object key)方法,则如下所示:

public Object get(Object key) {
    Object k = maskNull(key);
    int hash = hash(k);
    int i = indexFor(hash, table.length);
    Entry e = table[i];
    while (true) {
        if (e == null)
            return e;
        if (e.hash == hash && eq(k, e.key))
            return e.value;
        e = e.next;
    }
}

while(true){...}在多线程环境(IF)中始终会在运行时成为无限循环的受害者,因此e.next可以指向自身。这将导致无限循环。但是,如何e.next指向自身(即)。

这可能会在void transfer(Entry[] newTable)方法中发生,方法将在HashMap调整大小时调用。

do {
    Entry next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);

如果调整大小并且同时其他线程试图修改映射实例,则这段代码很容易产生上述条件。

避免这种情况的唯一方法是在代码中使用同步,或者更好的方法是使用同步集合。

解释抽象和封装?它们有什么关系?

抽象化

抽象仅捕获有关对象的与当前透视图有关的那些细节。

面向对象的编程理论中,抽象涉及定义代表抽象“角色”的对象的功能,这些抽象“角色”可以执行工作,报告和更改其状态,并与系统中的其他对象“通信”。

任何编程语言中的抽象都可以通过多种方式工作。从创建子例程到定义用于进行低级语言调用的接口可以看出。一些抽象试图通过完全隐藏它们所建立的抽象(例如设计模式)来限制程序员需要的概念的广度。

通常,可以通过两种方式查看抽象:

数据抽象是创建复杂数据类型并仅公开有意义的操作以与数据类型进行交互的方法,而隐藏所有实现细节均来自外部工作。

控件抽象是识别所有此类语句并将其作为工作单元公开的过程。我们通常在创建函数来执行任何工作时使用此功能。

封装形式

将类中的数据和方法与实现隐藏(通过访问控制)结合起来通常称为封装。结果是具有特征和行为的数据类型。封装本质上既有信息隐藏又有实现隐藏。

“ 无论发生什么变化,都将其封装 ”。它被引用为著名的设计原则。因此,在任何类中,运行时中的数据都可能发生更改,将来的发行版中可能会更改实现。因此,封装既适用于数据,也适用于实现。

因此,它们可以像下面这样关联:

–抽象更多是关于“类可以做什么”。[想法] –封装更多地是关于“如何”实现该功能的。[实施]

接口和抽象类之间的区别?

接口和抽象类之间的基本区别可以算作如下:

  • 接口不能有任何方法,而抽象类可以 [在Java 8默认方法之后不成立]
  • 一个类可以实现许多接口,但只能有一个超类(是否抽象)
  • 接口不属于类层次结构。不相关的类可以实现相同的接口

您应该记住:“当您可以用“ 它做什么 ”来充分描述该概念而无需指定“ 它如何工作 ”时,则应该使用一个接口。如果您需要包括一些实现细节,那么您将需要在抽象类中表示您的概念。”

另外,如果我说的不同:是否有许多类可以“ 分组 ”并用一个名词描述?如果是这样,请以该名词的名称创建一个抽象类,并从其继承这些类。例如,Cat并且Dog都可以从抽象类继承Animal,并且此抽象基类将实现一个方法void breathe(),所有动物因此将以完全相同的方式进行操作。

什么样的动词可以应用于我的班级,通常也可以应用于其他动词?为每个动词创建一个接口。例如,可以喂饱所有动物,因此我将创建一个名为的接口IFeedableAnimal实现该接口。只有DogHorse是不够好,虽然实现ILikeable,但有些则不是。

正如某人所说:主要区别在于您想要实现的地方。通过创建接口,可以将实现移动到实现接口的任何类。通过创建一个抽象类,您可以在一个中央位置共享所有派生类的实现,并且避免了很多不好的事情,例如代码重复。

StringBuffer如何节省内存?

字符串被实现为一个不可变的对象 ; 也就是说,当您最初决定将某些东西放入String对象中时,JVM会分配一个与初始值大小正好相等的固定宽度数组。然后,将其视为JVM内部的常量,在不更改String值的情况下,可以显着节省性能。但是,如果您决定以任何方式更改String的内容,那么JVM实质上要做的就是将原始String的内容复制到一个临时空间中,进行更改,然后将这些更改保存到一个新的内存阵列中。因此,在初始化后更改String的值是相当昂贵的操作。

另一方面,StringBuffer被实现为JVM内部可动态扩展的数组,这意味着任何更改操作都可以在现有内存位置上进行,而新内存仅按需分配。但是,JVM没有机会围绕进行优化StringBuffer,因为假定其内容在任何情况下都是可更改的。

为什么在对象类而不是线程中声明了wait and notify?

等待,通知,notifyAll方法仅在您希望线程访问共享资源并且共享资源可以是堆上的任何Java对象时才需要。因此,这些方法是在核心Object类上定义的,因此每个对象都可以控制允许线程在其监视器上等待。Java没有用于共享公共资源的任何特殊对象。没有这样的数据结构是defined.So,举证责任在Object类是考虑到能够成为共享资源提供会帮手等的方法wait()notify()notifyAll()

Java基于Hoare的监视器思想。在Java中,所有对象都有一个监视器。线程在监视器上等待,因此要执行等待,我们需要2个参数:

–线程
–监视器(任何对象)

在Java设计中,无法指定线程,它始终是当前运行代码的线程。但是,我们可以指定监视器(这是我们称为wait的对象)。这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,则会导致“入侵”,给设计/编程并发程序带来了困难。请记住,在Java中,不推荐使用任何会干扰另一个线程的执行的操作(例如stop())。

编写Java程序以在Java中创建死锁并修复死锁?

在Java中,死锁是指至少有两个线程在某个不同的资源上持有锁,并且都在等待其他资源来完成其任务的情况。而且,没有人能够锁定它所拥有的资源。

阅读更多:编写死锁并使用java解决

如果您的Serializable类包含一个不可序列化的成员,该怎么办?您如何解决?

在这种情况下,NotSerializableException将在运行时引发。要解决此问题,一个非常简单的解决方案是将此类字段标记为瞬态。这意味着这些字段将不会被序列化。如果还要保存这些字段的状态,则应考虑已经实现了Serializable接口的引用变量。

您可能还需要使用readResolve()writeResolve()方法。让我们总结一下:

  • 首先,使您的非序列化字段transient
  • 在中writeObject(),首先调用defaultWriteObject() Stream 以存储所有非瞬态字段,然后调用其他方法来序列化不可序列化对象的各个属性。
  • 在中readObject(),首先调用defaultReadObject() Stream 以读取所有非瞬态字段,然后调用其他方法(与您添加到writeObject的方法相对应)来反序列化不可序列化的对象。

另外,我强烈建议阅读有关java中序列化的完整指南

解释Java中的瞬态和volatile关键字?

短暂的

“ Java中瞬态关键字用于指示不应序列化字段。根据语言规范:可以标记变量transient以指示它们不是对象持久状态的一部分。例如,您可能具有从其他字段派生的字段,并且仅应以编程方式进行操作,而不要通过序列化来保持状态。

例如,在类BankPayment.java字段中,例如principalrate可以序列化,而interest即使在反序列化之后也可以随时计算。

回想一下,java中的每个线程也都有自己的本地内存空间,并且它在本地内存中执行所有读/写操作。完成所有操作后,它将所有线程访问该变量的位置写回主存储器中变量的修改状态。通常,这是JVM内部的默认 Stream 程。但是,volatile修饰符告诉JVM,访问该变量的线程必须始终将其自身的变量私有副本与内存中的主副本进行协调。这意味着每次线程想要读取变量的状态时,它都必须刷新其本地内存状态并从主内存中更新变量。

易挥发的

volatile在无锁算法中最有用。当您不使用锁定来访问该变量并且希望一个线程所做的更改在另一个线程中可见时,或者您要创建“后接”关系以确保计算是无需重新排序,以确保更改在适当的时间可见。

volatile应该用于安全地发布在多线程环境中不可变对象。声明诸如public volatileImmutableObject foo之类的字段可确保所有线程始终看到当前可用的实例引用。

Iterator和ListIterator之间的区别?

我们可以使用Iterator遍历a Set或a List或a Map。但ListIterator只能用于遍历List。其他差异如下所示。

您可以 –

  1. 向后迭代。
  2. 随时获取索引。
  3. 随时添加新值。
  4. 在这一点上设置一个新值。

例如

List<String> names = new ArrayList<String>();
names.add("Alex");
names.add("Bob");
names.add("Charles");
System.out.println(names);

ListIterator<String> listIterator = names.listIterator();

//Add a value at any place using ListIterator
while(listIterator.hasNext()){
    listIterator.next();
    listIterator.add("Lokesh");
}
System.out.println(names);

listIterator = names.listIterator();

//Set a value at any place using ListIterator
while(listIterator.hasNext()){
    listIterator.next();
    listIterator.set("John");
}
System.out.println(names);

Output:

[Alex, Bob, Charles]
[Alex, Lokesh, Bob, Lokesh, Charles, Lokesh]
[John, John, John, John, John, John]

显然,我可以在列表中的任意位置添加元素,同时对其进行迭代–同样,我也可以更改任何元素。使用Iterator,这是不可能的。

saigon has written 1440 articles

Leave a Reply