Java核心
9. Java中静态绑定和动态绑定有什么区别?
这个问题我在第18题关于静态多态和动态多态的这篇文章中已经回答过,建议你阅读一下。10. 是否可以在接口中使用私有或受保护的变量?
你不能。因为当你声明一个接口时,Java编译器会自动在接口方法之前添加public和abstract关键字,在数据成员之前添加public、static和final关键字。实际上,如果添加private或protected,就会产生冲突,编译器会抱怨访问修饰符,并显示以下消息:“Modifier '<selected modifier>' not allowed here.”为什么编译器要添加public、static和Final界面中的变量?我们来算一下:- public - 接口允许客户端与对象交互。如果变量不是公开的,客户端将无法访问它们。
- static - 无法创建接口(或者更确切地说,它们的对象),因此变量是静态的。
- Final - 由于该接口用于实现 100% 抽象,因此该变量具有其最终形式(并且不会更改)。
11.什么是类加载器以及它的用途是什么?
类加载器- 或类加载器 - 提供 Java 类的加载。更准确地说,加载是由其后代(特定的类加载器)确保的,因为 ClassLoader本身是抽象的。每次加载 .class 文件时,例如,在调用相应类的构造函数或静态方法之后,此操作都会由ClassLoader类的后代之一执行。继承人分为三种类型:-
Bootstrap ClassLoader是一个基本的加载器,在 JVM 级别实现,没有来自运行时环境的反馈,因为它是 JVM 内核的一部分并用本机代码编写。该加载器充当所有其他 ClassLoader 实例的父级。
主要负责加载JDK内部类,通常是位于$JAVA_HOME/jre/lib目录下的rt.jar等核心库。不同的平台可能对该类加载器有不同的实现。 -
扩展类加载器是一个扩展加载器,是基本加载器类的后代。负责加载标准 Java 基类的扩展。从 JDK 扩展目录加载,通常是$JAVA_HOME/lib/ext或 java.ext.dirs 系统属性中提到的任何其他目录(此选项可用于控制扩展的加载)。
-
System ClassLoader是在 JRE 级别实现的系统加载器,负责将所有应用程序级别的类加载到 JVM 中。它加载在类环境变量-classpath或-cp命令行选项中找到的文件。
-
系统类加载器尝试在其缓存中查找该类。
-
1.1. 如果找到该类,则加载成功完成。
-
1.2. 如果未找到该类,则将加载委托给扩展类加载器。
-
-
扩展类加载器尝试在自己的缓存中查找该类。
-
2.1. 如果找到该类,则它成功完成。
-
2.2. 如果未找到该类,则将加载委托给引导类加载器。
-
-
Bootstrap 类加载器尝试在自己的缓存中查找该类。
-
3.1. 如果找到该类,则加载成功完成。
-
3.2. 如果找不到该类,底层的 Bootstrap 类加载器将尝试加载它。
-
-
如果加载:
-
4.1. 成功 - 类加载完成。
-
4.2. 如果失败,控制权将转移到扩展类加载器。
-
-
5.扩展类加载器尝试加载类,如果加载:
-
5.1. 成功 - 类加载完成。
-
5.2. 如果不成功,控制权将转移到系统类加载器。
-
-
6.系统类加载器尝试加载类,如果加载:
-
6.1. 成功 - 类加载完成。
-
6.2. 没有成功通过——产生异常——ClassNotFoundException。
-
12. 什么是运行时数据区?
Run-Time Data Ares - JVM 运行时数据区域。JVM定义了程序执行过程中需要的一些运行时数据区域。其中一些是在 JVM 启动时创建的。其他的是线程本地的,仅在创建线程时创建(并在线程销毁时销毁)。JVM 运行时数据区域如下所示:-
PC寄存器是每个线程本地的,包含线程当前正在执行的JVM指令的地址。
-
JVM 堆栈是一个内存区域,用于存储局部变量和临时结果。每个线程都有自己独立的堆栈:一旦线程终止,该堆栈也会被销毁。值得注意的是,栈相对于堆的优势在于性能,而堆在存储规模上当然更有优势。
-
本机方法堆栈 - 存储数据元素的每线程数据区域,类似于 JVM 堆栈,用于执行本机(非 Java)方法。
-
堆 - 被所有线程用作存储,其中包含在运行时创建的对象、类元数据、数组等。该区域在 JVM 启动时创建,在 JVM 关闭时销毁。
-
方法区 - 该运行时区域对所有线程都是公共的,并且在 JVM 启动时创建。它存储每个类的结构,例如运行时常量池、构造函数和方法的代码、方法数据等。
13.什么是不可变对象?
文章这一部分,第14题和第15题中,已经有这个问题的答案了,看一下,别浪费时间了。14. String类有什么特别之处?
在前面的分析中,我们反复讨论了String 的某些特性(对此有一个单独的部分)。现在我们来总结一下String的特点:-
它是 Java 中最流行的对象,有多种用途。从使用频率上来说,甚至不逊色于原始类型。
-
无需使用 new 关键字即可创建此类的对象 - 直接通过引号String str = “string”; 。
-
String是一个不可变的类:当创建该类的对象时,它的数据不能改变(当你在某个字符串上添加+“另一个字符串”时,结果你将得到一个新的第三个字符串)。String 类的不变性使其成为线程安全的。
-
String类是finalized的(有final修饰符),所以不能被继承。
-
String有自己的字符串池,它是堆中的一块内存区域,用于缓存它创建的字符串值。在本系列的这一部分中,在问题 62 中,我描述了字符串池。
-
Java 有String 的类似物,也设计用于处理字符串 - StringBuilder和StringBuffer,但不同之处在于它们是可变的。您可以在本文中阅读有关它们的更多信息。
15.什么是类型协方差?
为了理解协方差,我们将看一个例子。假设我们有一个动物类:public class Animal {
void voice() {
System.out.println("*тишина*");
}
}
还有一些Dog类扩展它:
public class Dog extends Animal {
@Override
public void voice() {
System.out.println("Гав, гав, гав!!!");
}
}
我们记得,我们可以轻松地将继承类型的对象分配给父类型:
Animal animal = new Dog();
这只不过是多态性。是不是方便、灵活?那么,动物名单呢?我们可以给出一个包含通用Animal 的列表和包含Dog对象的列表吗?
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;
在这种情况下,将狗列表分配给动物列表的行将以红色下划线显示,即 编译器不会通过此代码。尽管这个赋值看起来很合乎逻辑(毕竟,我们可以将Dog对象赋值给Animal类型的变量),但它无法完成。这是因为如果允许的话,我们将能够将Animal对象放入最初打算成为Dog 的列表中,同时认为列表中只有Dogs。然后,例如,我们将使用get()方法从狗列表中获取一个对象,认为它是一只狗,并在其上调用Dog对象的某些方法,这是Animal没有的。正如您所理解的,这是不可能的 - 将会发生错误。但是,幸运的是,编译器不会错过将后代列表分配给父级列表的逻辑错误(反之亦然)。在 Java 中,您只能将列表对象分配给具有匹配泛型的列表变量。这称为不变性。如果他们能做到这一点,那就被称为协方差。也就是说,协方差是指我们是否可以将ArrayList<Dog>类型的对象设置为List<Animal>类型的变量。原来Java中不支持协方差?不管怎么样! 但这是以它自己特殊的方式完成的。设计是用来做什么的?扩展了 Animal。它与我们想要设置列表对象的变量的泛型以及后代的泛型一起放置。这种通用构造意味着任何Animal类型的后代类型都可以(并且Animal类型也属于这种概括)。反过来,Animal不仅可以是一个类,还可以是一个接口(不要被extends关键字所迷惑)。我们可以像这样完成之前的任务:
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
因此,您将在 IDE 中看到编译器不会抱怨此构造。让我们检查一下这个设计的功能。假设我们有一个方法,可以让传递给它的所有动物发出声音:
public static void animalsVoice(List<? extends Animal> animals) {
for (Animal animal : animals) {
animal.voice();
}
}
让我们给他一份狗的清单:
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
animalsVoice(dogs);
在控制台中我们将看到以下输出:
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
animals.add(new Dog());
dogs.add(new Animal());
实际上,在最后两行中,编译器将以红色突出显示对象的插入。这是因为我们无法百分百确定哪个类型的对象列表将通过通用<? 扩展动物>。 我还想谈谈逆变,因为通常这个概念总是与协变联系在一起,并且通常他们会一起被问到。这个概念在某种程度上与协方差相反,因为该构造使用继承人类型。假设我们想要一个列表,该列表可以分配不是Dog对象祖先的类型对象列表。然而,我们事先并不知道这些具体类型是什么。在这种情况下,形式的构造?super Dog,所有类型都适合 - Dog类的祖先:
List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals;
dogs.add(new Dog());
dogs.add(new Dog());
我们可以使用这样的泛型安全地将Dog类型的对象添加到列表中,因为在任何情况下它都具有其任何祖先的所有实现方法。但是我们将无法添加Animal类型的对象,因为不能确定内部是否存在该类型的对象,而不是Dog等类型的对象。毕竟,我们可以从此列表的元素请求Dog类的方法,而Animal则没有。在这种情况下,会出现编译错误。另外,如果我们想实现前面的方法,但使用这个泛型:
public static void animalsVoice(List<? super Dog> dogs) {
for (Dog dog : dogs) {
dog.voice();
}
}
我们会在for循环 中遇到编译错误,因为我们无法确定返回的列表包含Dog类型的对象并且可以自由使用其方法。如果我们调用这个列表上的dogs.get(0)方法。- 我们将得到一个Object 类型的对象。也就是说,为了让AnimalsVoice()方法起作用,我们至少需要添加一些小的操作来缩小类型数据:
public static void animalsVoice(List<? super Dog> dogs) {
for (Object obj : dogs) {
if (obj instanceof Dog) {
Dog dog = (Dog) obj;
dog.voice();
}
}
}
GO TO FULL VERSION