各位软件工程师,女士们先生们大家好!我们来谈谈面试问题。关于您需要准备什么以及您需要了解什么。这是从头开始重复或研究这些要点的绝佳理由。 我收集了相当广泛的有关 OOP、Java 语法、Java 中的异常、集合和多线程的常见问题,为了方便起见,我将其分为几个部分。 重要的:我们只会讨论 Java 版本 8 之前的版本。这里不会考虑 9、10、11、12、13 中的所有创新。欢迎任何关于如何改进答案的想法/评论。祝阅读愉快,走吧!
Java面试:OOP问题
1.Java有什么特点?
回答:-
面向对象编程的概念:
- 面向对象;
- 遗产;
- 封装;
- 多态性;
- 抽象。
-
跨平台: Java程序无需任何修改就可以在任何平台上运行。您唯一需要的是已安装的 JVM(java 虚拟机)。
-
高性能: JIT(Just In Time编译器)允许高性能。JIT 将字节码转换为机器码,然后 JVM 开始执行。
- 多线程:称为 的执行线程
Thread
。JVM 创建一个名为 的线程main thread
。程序员可以通过继承 Thread 类或实现接口来创建多个线程Runnable
。
2.什么是继承?
继承意味着一个类可以继承(“扩展”)另一个类。这样您就可以重用您继承的类中的代码。现有类称为superclass
,正在创建的类称为subclass
。他们还说parent
和child
。
public class Animal {
private int age;
}
public class Dog extends Animal {
}
其中Animal
是parent
,并且Dog
- child
。
3.什么是封装?
这个问题在 Java 开发人员面试中经常出现。封装是使用访问修饰符、getter 和 setter 来隐藏实现。这样做是为了在开发人员认为有必要的地方关闭外部使用的访问。生活中一个容易理解的例子是汽车。我们无法直接了解发动机的运行情况。对于我们来说,工作就是将钥匙插入点火开关并启动发动机。幕后将发生什么流程与我们无关。此外,我们对这项活动的干扰可能会导致不可预测的情况,因此我们可能会损坏汽车并伤害自己。编程中也发生同样的事情。维基百科上有很好的描述。JavaRush上也有一篇关于封装的文章。4.什么是多态性?
多态性是程序能够以相同的接口相同地使用对象,而无需了解该对象的特定类型。正如他们所说,一个接口 - 多种实现。通过多态性,您可以根据不同类型的对象的共同行为来组合和使用它们。例如,我们有一个 Animal 类,它有两个后代 - Dog 和 Cat。通用 Animal 类有一个共同的行为 - 发出声音。当我们需要将 Animal 类的所有后代放在一起并执行“发出声音”方法时,我们可以使用多态性的可能性。它将如下所示:List<Animal> animals = Arrays.asList(new Cat(), new Dog(), new Cat());
animals.forEach(animal -> animal.makeSound());
所以多态性对我们有帮助。此外,这也适用于多态(重载)方法。 使用多态性的实践
面试问题 - Java 语法
5. Java中的构造函数是什么?
以下特征有效:- 创建新对象时,程序会使用适当的构造函数来执行此操作。
- 构造函数就像一个方法。它的奇特之处在于没有返回元素(包括void),并且它的名称与类的名称相同。
- 如果没有明确编写构造函数,则会自动创建一个空构造函数。
- 构造函数可以被重写。
- 如果创建了带参数的构造函数,但还需要一个不带参数的构造函数,则需要单独编写它,因为它不会自动创建。
6. 哪两个类不是继承自Object?
不要被挑衅所迷惑,不存在这样的类:所有直接或通过祖先的类都是从 Object 类继承的!7.什么是局部变量?
Java 开发人员面试中的另一个常见问题。局部变量是在方法内部定义的变量,并且在方法执行之前一直存在。一旦执行结束,局部变量将不复存在。下面是一个在 main() 方法中使用 helloMessage 局部变量的程序:public static void main(String[] args) {
String helloMessage;
helloMessage = "Hello, World!";
System.out.println(helloMessage);
}
8.什么是实例变量?
实例变量是在类内部定义的变量,它一直存在到对象存在的那一刻。一个例子是 Bee 类,它有两个变量 nectarCapacity 和 maxNectarCapacity:public class Bee {
/**
* Current nectar capacity
*/
private double nectarCapacity;
/**
* Maximal nectar that can take bee.
*/
private double maxNectarCapacity = 20.0;
...
}
9. 什么是访问修饰符?
访问修饰符是一种允许您自定义对类、方法和变量的访问的工具。有以下修饰符,按访问权限增加的顺序排列:private
- 用于方法、字段和构造函数。访问级别只是声明它的类。package-private(default)
- 可用于课程。仅在声明了类、方法、变量、构造函数的特定包中访问。protected
package-private
—对于那些从带有修饰符的类继承的类,具有与 + 相同的访问权限protected
。public
- 也用于课程。整个应用程序的完全访问权限。
10.什么是重写方法?
当子类想要更改父类的行为时,就会发生方法重写。如果你想要执行父方法中的内容,你可以在子方法中使用像 super.methodName() 这样的构造,它将完成父方法的工作,然后才添加逻辑。需满足的要求:- 方法签名必须相同;
- 返回值应该是相同的。
11.什么是方法签名?
方法签名是方法名称和方法接受的参数的集合。方法签名是重载方法时方法的唯一标识符。12.什么是方法重载?
方法重载是多态性的一个属性,通过更改方法签名,您可以为相同的操作创建不同的方法:- 相同的方法名称;
- 不同的论点;
- 可能有不同的返回类型。
add()
内容ArrayList
,并将根据传入参数以不同的方式执行加法:
add(Object o)
- 简单地添加一个对象;add(int index, Object o)
— 将对象添加到特定索引;add(Collection<Object> c)
— 添加对象列表;add(int index, Collection<Object> c)
— 从某个索引开始添加对象列表。
13.什么是接口?
Java 中没有实现多重继承,因此为了解决这个问题,添加了我们所知道的接口;) 很长一段时间,接口只有方法而没有实现它们。作为这个答案的一部分,我们将讨论它们。例如:
public interface Animal {
void makeSound();
void eat();
void sleep();
}
由此得出一些细微差别:
- 接口中的所有方法都是公共的、抽象的;
- 所有变量都是public static final;
- 类并不继承它们(扩展),而是实现它们(实现)。此外,您可以实现任意数量的接口。
- 实现接口的类必须提供该接口所具有的所有方法的实现。
public class Cat implements Animal {
public void makeSound() {
// method implementation
}
public void eat() {
// implementation
}
public void sleep() {
// implementation
}
}
14. 接口中的默认方法是什么?
现在我们来谈谈默认方法。为了什么,为了谁?添加这些方法是为了让一切“既是你的,也是我们的”。我在说什么?是的,一方面,有必要添加新功能:lambdas、Stream API,另一方面,有必要保留 Java 闻名的东西——向后兼容性。为此,有必要在界面中引入现成的解决方案。这就是我们如何使用默认方法的。也就是说,默认方法是接口中具有关键字 的实现方法default
。例如,众所周知的stream()
方法Collection
。看看吧,这个界面并不像看起来那么简单;)。forEach()
或者也是来自 的同样众所周知的方法Iterable
。在添加默认方法之前它也不存在。顺便说一句,您还可以在JavaRush上阅读有关此内容的内容。
15. 那么如何继承两个相同的默认方法呢?
根据之前关于默认方法是什么的答案,您可以问另一个问题。如果你可以在接口中实现方法,那么理论上你可以用相同的方法实现两个接口,如何做到这一点?有两个不同的接口具有相同的方法:interface A {
default void foo() {
System.out.println("Foo A");
}
}
interface B {
default void foo() {
System.out.println("Foo B");
}
}
并且有一个类实现了这两个接口。foo()
为了避免不确定性并编译代码,我们需要重写类中的方法,并且我们可以简单地调用其中任何接口的C
方法-或。但究竟如何选择具体的接口方法呢?有一个这样的结构: foo()
A
B
А
В
A.super.foo()
public class C implements A, B {
@Override
public void foo() {
A.super.foo();
}
}
或者:
public class C implements A, B {
@Override
public void foo() {
B.super.foo();
}
}
因此,foo()
类方法将使用接口中的C
默认方法或接口中的方法。 foo()
A
foo()
B
16.什么是抽象方法和类?
Java 有一个保留字abstract
,用于表示抽象类和方法。首先,一些定义。abstract
抽象方法是在抽象类中使用关键字创建的没有实现的方法。也就是说,这是一个像接口中的方法,只是添加了一个关键字,例如:
public abstract void foo();
抽象类是一个也有abstract
这个词的类:
public abstract class A {
}
抽象类有几个特点:
- 不能在其基础上创建对象;
- 它可以有抽象方法;
- 它可能没有抽象方法。
17. String、String Builder 和 String Buffer 有什么区别?
这些值String
存储在常量字符串池中。一旦创建了一行,它将出现在该池中。并且将无法删除它。例如:
String name = "book";
...变量将引用字符串池常量字符串池 如果将变量名称设置为不同的值,您将得到以下结果:
name = "pen";
常量字符串池 所以这两个值会保留在那里。 字符串缓冲区:
- 值
String
存储在堆栈中。如果该值发生变化,则新值将被旧值替换; String Buffer
同步,因此线程安全;- 由于线程安全的原因,运行速度还有很多不尽如人意的地方。
StringBuffer name = "book";
一旦 name 的值发生变化,堆栈上的值就会发生变化: StringBuilder 与 完全相同StringBuffer
,只是它不是线程安全的。因此,它的速度明显高于StringBuffer
。
18. 抽象类和接口有什么区别?
抽象类:- 抽象类有一个默认构造函数;每次创建该抽象类的子类时都会调用它;
- 包含抽象方法和非抽象方法。总的来说,它可能不包含抽象方法,但仍然是一个抽象类;
- 从抽象类继承的类必须仅实现抽象方法;
- 抽象类可以包含实例变量(参见问题#5)。
- 没有构造函数,无法初始化;
- 只应该添加抽象方法(不包括默认方法);
- 实现接口的类必须实现所有方法(不包括默认方法);
- 接口只能包含常量。
19. 为什么访问数组中的元素需要 O(1)?
这个问题实际上来自上次采访。后来我才知道,问这个问题是为了看看一个人是怎么想的。显然,这些知识没有什么实际意义:只要知道这个事实就足够了。首先,我们需要澄清 O(1) 是当操作在恒定时间内发生时算法的时间复杂度的指定。也就是说,这个指定是最快执行的。要回答这个问题,我们需要了解一下我们对数组了解多少?要创建数组int
,我们必须编写以下内容:
int[] intArray = new int[100];
从这段录音中可以得出几个结论:
- 创建数组时,它的类型是已知的。如果类型已知,那么数组的每个单元的大小就很清楚了。
- 数组的大小是已知的。
如何以 O(1) 的速度访问 ArrayList 中的对象?
这个问题紧接着上一个问题。确实,当我们使用数组并且其中有基元时,我们在创建该类型时提前知道该类型的大小是多少。但是,如果有一种如图所示的方案: 我们想要创建一个包含 A 类型元素的集合,并添加不同的实现 - B、C、D:List<A> list = new ArrayList();
list.add(new B());
list.add(new C());
list.add(new D());
list.add(new B());
在这种情况下,您如何了解每个单元格的大小,因为每个对象都会不同,并且可能具有不同的附加字段(或完全不同)。该怎么办?这里提出这个问题,是为了混淆视听。我们知道,其实集合并不存储对象,而只是存储这些对象的链接。所有链接都具有相同的大小,这是已知的。因此,这里计算空间的方式与上一个问题相同。
21. 自动装箱和拆箱
历史背景:自动装箱和自动拆箱是 JDK 5 的主要创新之一。 自动装箱是从原始类型自动转换为适当的包装类的过程。 自动拆箱- 与自动装箱完全相反 - 将包装类转换为基元。但如果存在包装器值null
,则在解包期间将引发异常NullPointerException
。
匹配原语 - 包装器
原始 | 类包装器 |
---|---|
布尔值 | 布尔值 |
整数 | 整数 |
字节 | 字节 |
字符 | 特点 |
漂浮 | 漂浮 |
长的 | 长的 |
短的 | 短的 |
双倍的 | 双倍的 |
自动打包发生:
-
当为原语分配对包装类的引用时:
Java 5 之前:
//manual packaging or how it was BEFORE Java 5. public void boxingBeforeJava5() { Boolean booleanBox = new Boolean(true); Integer intBox = new Integer(3); // and so on to other types } после Java 5: //automatic packaging or how it became in Java 5. public void boxingJava5() { Boolean booleanBox = true; Integer intBox = 3; // and so on to other types }
-
当将原语作为参数传递给需要包装器的方法时:
public void exampleOfAutoboxing() { long age = 3; setAge(age); } public void setAge(Long age) { this.age = age; }
自动解包发生:
-
当我们将一个原始变量分配给包装类时:
//before Java 5: int intValue = new Integer(4).intValue(); double doubleValue = new Double(2.3).doubleValue(); char c = new Character((char) 3).charValue(); boolean b = Boolean.TRUE.booleanValue(); //and after JDK 5: int intValue = new Integer(4); double doubleValue = new Double(2.3); char c = new Character((char) 3); boolean b = Boolean.TRUE;
-
在进行算术运算的情况下。它们仅适用于原始类型;为此您需要对原始类型进行解包。
// Before Java 5 Integer integerBox1 = new Integer(1); Integer integerBox2 = new Integer(2); // for comparison it was necessary to do this: integerBox1.intValue() > integerBox2.intValue() //в Java 5 integerBox1 > integerBox2
-
当传递给接受相应原语的方法中的包装器时:
public void exampleOfAutoboxing() { Long age = new Long(3); setAge(age); } public void setAge(long age) { this.age = age; }
22.final关键字是什么以及在哪里使用它?
该关键字final
可用于变量、方法和类。
- 最终变量不能重新分配给另一个对象。
- 最终类是不育的))它不能有继承人。
- 最终方法不能在祖先上被重写。
最终变量
;Java 为我们提供了两种创建变量并为其赋值的方法:- 您可以声明一个变量并稍后对其进行初始化。
- 您可以声明一个变量并立即分配它。
public class FinalExample {
//final static variable, which is immediately initialized:
final static String FINAL_EXAMPLE_NAME = "I'm likely final one";
//final is a variable that is not initialized, but will only work if
//initialize this in the constructor:
final long creationTime;
public FinalExample() {
this.creationTime = System.currentTimeMillis();
}
public static void main(String[] args) {
FinalExample finalExample = new FinalExample();
System.out.println(finalExample.creationTime);
// final field FinalExample.FINAL_EXAMPLE_NAME cannot be assigned
// FinalExample.FINAL_EXAMPLE_NAME = "Not you're not!";
// final field Config.creationTime cannot be assigned
// finalExample.creationTime = 1L;
}
}
Final 变量可以被视为常量吗?
由于我们无法为最终变量分配新值,因此这些似乎是常量变量。但这只是乍一看。如果变量引用的数据类型是immutable
,那么是的,它是一个常量。但是,如果数据类型mutable
是可变的,则使用方法和变量就可以更改变量引用的对象的值final
,在这种情况下,它不能称为常量。因此,该示例表明某些最终变量确实是常量,但有些则不是,并且它们是可以更改的。
public class FinalExample {
//immutable final variables:
final static String FINAL_EXAMPLE_NAME = "I'm likely final one";
final static Integer FINAL_EXAMPLE_COUNT = 10;
// mutable filter variables
final List<String> addresses = new ArrayList();
final StringBuilder finalStringBuilder = new StringBuilder("constant?");
}
局部最终变量
当final
一个变量在方法内部创建时,它被称为local final
变量:
public class FinalExample {
public static void main(String[] args) {
// This is how you can
final int minAgeForDriveCar = 18;
// or you can do it this way, in the foreach loop:
for (final String arg : args) {
System.out.println(arg);
}
}
}
final
我们可以在扩展循环中 使用关键字for
,因为完成循环迭代后,for
每次都会创建一个新变量。但这不适用于普通的 for 循环,因此下面的代码将引发编译时错误。
// final local changed j cannot be assigned
for (final int i = 0; i < args.length; i ++) {
System.out.println(args[i]);
}
最后一堂课
您不能扩展声明为 的类final
。简而言之,没有任何类可以继承这个类。final
JDK 中的类的一个很好的例子是String
. 创建不可变类的第一步是将其标记为final
,以便它无法扩展:
public final class FinalExample {
}
// Compilation error here
class WantsToInheritFinalClass extends FinalExample {
}
最终方法
当一个方法被标记为final时,它被称为final方法(逻辑上的,对吧?)。Final 方法不能在后代类中被重写。顺便说一句,Object 类中的方法 - wait() 和 notification() - 是最终方法,因此我们没有机会重写它们。public class FinalExample {
public final String generateAddress() {
return "Some address";
}
}
class ChildOfFinalExample extends FinalExample {
// compile error here
@Override
public String generateAddress() {
return "My OWN Address";
}
}
在 Java 中如何以及在何处使用 Final
- 使用final关键字定义一些类级别的常量;
- 当您不希望对象被修改时,为对象创建最终变量。例如,我们可以用于记录目的的特定于对象的属性;
- 如果您不想延长该课程,请将其标记为最终课程;
- 如果你需要创建一个不可变的<类,你需要将其设为final;
- 如果您希望方法的实现在其后代中不发生更改,请将该方法指定为
final
。这对于确保实施不会改变非常重要。
23.什么是可变不可变?
可变的
可变对象是其状态和变量在创建后可以更改的对象。例如StringBuilder、StringBuffer等类。例子:public class MutableExample {
private String address;
public MutableExample(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
// this setter can change the name field
public void setAddress(String address) {
this.address = address;
}
public static void main(String[] args) {
MutableExample obj = new MutableExample("first address");
System.out.println(obj.getAddress());
// update the name field, so this is a mutable object
obj.setAddress("Updated address");
System.out.println(obj.getAddress());
}
}
不可变的
不可变对象是指在创建对象后其状态和变量不能更改的对象。为什么 HashMap 没有一个很好的键,对吧?)例如 String、Integer、Double 等等。例子:// make this class final so no one can change it
public final class ImmutableExample {
private String address;
ImmutableExample (String address) {
this.address = address;
}
public String getAddress() {
return address;
}
//remove the setter
public static void main(String[] args) {
ImmutableExample obj = new ImmutableExample("old address");
System.out.println(obj.getAddress());
// Therefore, do not change this field in any way, so this is an immutable object
// obj.setName("new address");
// System.out.println(obj.getName());
}
}
24.如何编写不可变类?
弄清楚什么是可变对象和不可变对象后,下一个问题就很自然了——怎么写?要编写一个不可变的不可变类,您需要遵循简单的步骤:- 使课程成为最终的。
- 将所有字段设为私有并只为它们创建 getter。当然,不需要设置器。
- 将所有可变字段设为最终值,以便该值只能设置一次。
- 通过构造函数初始化所有字段,执行深复制(即复制对象本身、其变量、变量的变量等)
- 在 getter 中克隆可变变量对象以仅返回值的副本,而不是对实际对象的引用。
/**
* An example of creating an immutable object.
*/
public final class FinalClassExample {
private final int age;
private final String name;
private final HashMap<String, String> addresses;
public int getAge() {
return age;
}
public String getName() {
return name;
}
/**
* Clone the object before returning it.
*/
public HashMap<String, String> getAddresses() {
return (HashMap<String, String>) addresses.clone();
}
/**
* In the constructor, deep copy the mutable objects.
*/
public FinalClassExample(int age, String name, HashMap<String, String> addresses) {
System.out.println("Performing a deep copy in the constructor");
this.age = age;
this.name = name;
HashMap<String, String> temporaryMap = new HashMap<>();
String key;
Iterator<String> iterator = addresses.keySet().iterator();
while (iterator.hasNext()) {
key = iterator.next();
temporaryMap.put(key, addresses.get(key));
}
this.addresses = temporaryMap;
}
}
GO TO FULL VERSION