java基础
# Java 基础
# JDK、JRE、JVM之间的区别
# Java有哪些数据类型
# Java中的拆箱装箱
# & 和 &&、| 和 ||之间的区别
& 是按位与运算符,它可以用于整数类型和布尔类型。当用于布尔类型时,执行逻辑与运算。同时为true,结果为true,否则为false。 && 是短路与运算符,只能用于布尔类型。当第一个为false的时候,就不会计算第二个操作数的值了。 | 与 ||之间的区别同理。
# 面向对象
# 面向对象与面向过程的区别
面向过程编程和面向对象编程是两种不同的编程思想。面向过程编程强调的是功能行为,以函数为最小单位,考虑怎么做。而面向对象编程将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。
面向过程编程是面向对象编程的基础,面向过程过程是解决唯一的不重复的工作,面向对象是尽可能的避免冗余的工作¹。面向对象具有三大特征:封装性、继承性和多态性。
这两种编程思想都有其优缺点。简单来说,面向过程编程流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析。效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。但缺点是需要深入的思考,耗费精力,代码重用性低,扩展能力差,后期维护难度比较大。
而面向对象编程结构清晰,程序是模块化和结构化,更加符合人类的思维方式;易扩展,代码重用率高,可继承,可覆盖,可以设计出低耦合的系统;易维护,系统低耦合的特点有利于减少程序的后期维护工作量。但缺点是开销大,在要修改对象内部时,对象的属性不允许外部直接存取,所以要增加许多没有其他意义、只负责读或写的行为。这会为编程工作增加负担,增加运行开销,并且使程序显得臃肿。性能低,由于面向更高的逻辑抽象层,使得面向对象在实现的时候不得不做出性能上面的牺牲。
# 面向对象的特性
# 重载(overload)和重写(override)的区别?
# 访问修饰符 public、private、protected、以及不写(默认)时的区别?
访问修饰符用来定义类、方法或者变量的访问权限。Java 支持 4 种不同的访问权限:public
,protected
,private
和 default
(即默认,什么也不写)。
访问修饰符的目的是保护类、变量、方法和构造方法的访问。它们可以控制哪些类或对象可以访问特定的成员。例如,private
修饰符可以防止类的外部直接访问类的内部成员,而 public
修饰符则允许任何类或对象访问特定成员。
使用访问修饰符可以提高代码的可维护性和安全性。它们可以帮助程序员更好地组织代码,并防止意外修改或访问敏感数据。
# 抽象类(abstract class) 和接口(interface)之间的区别
抽象类和接口都是Java中支持抽象的机制,它们都可以用来定义抽象的类型和方法。但是,它们之间也有一些重要的区别,这就是为什么Java中既有抽象类又有接口的原因。
实现方式不同:抽象类可以包含抽象方法和具体方法,而接口中只能包含抽象方法。这意味着,抽象类可以为子类提供一些默认的实现,而接口则需要子类实现所有的方法。
继承方式不同:一个类只能继承一个抽象类,但是可以实现多个接口。这意味着,接口可以用来实现多重继承,而抽象类则不能。
成员变量不同:抽象类可以包含成员变量,而接口中只能包含静态常量。这意味着,抽象类可以定义一些状态信息,而接口则不能。
总之,抽象类和接口都是Java中支持抽象的机制,它们各有优缺点和适用场景。在设计程序时,应根据具体需求选择使用抽象类还是接口。
# 为什么会有final关键字啊
final
关键字在Java中用于表示一个变量、方法或类是不可变的。它可以用来实现不同的目的:
- 防止修改:当
final
用于修饰变量时,它表示该变量的值在初始化后不能再被修改。这可以用来定义常量,或者防止意外修改变量的值。
final int x = 10;
x = 20; // 编译错误,不能修改final变量的值
2
- 防止重写:当
final
用于修饰方法时,它表示该方法不能被子类重写。这可以用来防止子类改变父类方法的行为。
class MyClass {
public final void myMethod() {
// ...
}
}
class MySubClass extends MyClass {
public void myMethod() { // 编译错误,不能重写final方法
// ...
}
}
2
3
4
5
6
7
8
9
10
11
- 防止继承:当
final
用于修饰类时,它表示该类不能被继承。这可以用来防止其他类继承该类并修改它的行为。
final class MyClass {
// ...
}
class MySubClass extends MyClass { // 编译错误,不能继承final类
// ...
}
2
3
4
5
6
7
总之,final
关键字在Java中用于表示不可变性,它可以用来防止修改变量的值、重写方法或继承类。这样可以提高程序的安全性和可靠性。
# == 与 equals之间的区别
==
运算符和equals
方法都可以用来比较两个对象是否相等,但它们的比较方式不同。
- ==运算符:
==
运算符用于比较两个变量的值是否相等。对于基本类型,它比较的是变量的值;对于引用类型,它比较的是变量所引用的对象的内存地址。因此,当使用==
运算符比较两个引用类型变量时,只有当它们指向同一个对象时才会返回true
。
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // 输出 false
2
3
- equals方法:
equals
方法用于比较两个对象的内容是否相等。它是一个方法,因此可以被子类重写,以实现自定义的比较逻辑。例如,在String
类中,equals
方法被重写,用于比较两个字符串的内容是否相等。
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1.equals(s2)); // 输出 true
2
3
总之,==
运算符和equals
方法都可以用来比较两个对象是否相等,但它们的比较方式不同。在使用时,应根据具体需求选择合适的比较方式。
# 为什么重写equals时必须重写hashCode方法
当重写equals
方法时,通常也需要重写hashCode
方法,以保持两者之间的一致性。这是因为hashCode
方法的返回值通常用于确定对象在哈希表(如HashSet
、HashMap
等)中的存储位置。如果两个对象相等(即它们的equals
方法返回true
),那么它们的hashCode
方法应该返回相同的值,以便它们能够被存储在哈希表的同一个位置。
如果重写了equals
方法但没有重写hashCode
方法,那么可能会出现两个相等的对象具有不同的哈希码,从而导致它们被错误地存储在哈希表的不同位置。这会破坏哈希表的正确性,导致程序出现错误。
例如,假设我们定义了一个表示点的类Point
,并重写了它的equals
方法,但没有重写它的hashCode
方法:
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Point) {
Point other = (Point) obj;
return x == other.x && y == other.y;
}
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后,我们创建两个内容相等但内存地址不同的点对象,并将它们添加到一个哈希集合中:
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println(set.size()); // 输出 2
2
3
4
由于我们没有重写hashCode
方法,所以两个点对象具有不同的哈希码,从而被错误地存储在哈希集合的不同位置。这导致哈希集合中出现了两个内容相等但内存地址不同的点对象,破坏了哈希集合的正确性。
因此,当重写equals
方法时,通常也需要重写hashCode
方法,以保持两者之间的一致性。
# 深拷贝 与 浅拷贝
深拷贝和浅拷贝是两种不同的对象复制方式。
- 浅拷贝:浅拷贝只复制对象的引用,而不复制对象本身。这意味着,如果一个对象包含对其他对象的引用,那么浅拷贝后,这两个对象仍然会共享同一个引用对象。
class MyClass {
public int[] data;
public MyClass(int[] data) {
this.data = data;
}
public MyClass shallowCopy() {
return new MyClass(data);
}
}
int[] data = {1, 2, 3};
MyClass obj1 = new MyClass(data);
MyClass obj2 = obj1.shallowCopy();
obj2.data[0] = 10;
System.out.println(obj1.data[0]); // 输出 10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 深拷贝:深拷贝不仅复制对象的引用,还会复制对象本身。这意味着,如果一个对象包含对其他对象的引用,那么深拷贝后,这两个对象将不再共享同一个引用对象。
class MyClass {
public int[] data;
public MyClass(int[] data) {
this.data = data;
}
public MyClass deepCopy() {
int[] newData = Arrays.copyOf(data, data.length);
return new MyClass(newData);
}
}
int[] data = {1, 2, 3};
MyClass obj1 = new MyClass(data);
MyClass obj2 = obj1.deepCopy();
obj2.data[0] = 10;
System.out.println(obj1.data[0]); // 输出 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
深拷贝和浅拷贝各有优缺点和适用场景。浅拷贝速度快,但可能会导致数据共享问题;深拷贝可以避免数据共享问题,但速度较慢。在使用时,应根据具体需求选择合适的拷贝方式。
# Java创建对象的方式有哪些
在Java中,可以使用以下几种方式创建对象:
- 使用new关键字:这是最常用的创建对象的方式。使用new关键字可以调用类的构造函数来创建一个新的对象。
MyClass obj = new MyClass();
- 使用Class类的newInstance方法:可以使用Class类的newInstance方法来创建一个类的实例。这种方式需要获取类的Class对象,然后调用它的newInstance方法。
MyClass obj = MyClass.class.newInstance();
- 使用Constructor类的newInstance方法:可以使用Constructor类的newInstance方法来创建一个类的实例。这种方式需要获取类的构造函数,然后调用它的newInstance方法。
Constructor<MyClass> constructor = MyClass.class.getConstructor();
MyClass obj = constructor.newInstance();
2
- 使用clone方法:如果一个类实现了Cloneable接口,那么可以使用它的clone方法来创建一个新的对象。这种方式会创建一个新的对象,并复制原对象的内容。
MyClass obj1 = new MyClass();
MyClass obj2 = (MyClass) obj1.clone();
2
- 使用反序列化:如果一个类实现了Serializable接口,那么可以通过反序列化来创建一个新的对象。这种方式需要将对象序列化到一个流中,然后再从流中反序列化出一个新的对象。
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj1);
oos.flush();
oos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
MyClass obj2 = (MyClass) ois.readObject();
ois.close();
2
3
4
5
6
7
8
9
# String、StringBuilder、StringBuffer区别
String
、StringBuilder
和StringBuffer
都是Java中表示字符串的类,但它们之间有一些重要的区别。
不可变性:
String
类是不可变的,这意味着一旦创建了一个字符串对象,它的内容就不能再被修改。而StringBuilder
和StringBuffer
类都是可变的,它们提供了许多方法来修改字符串的内容。性能:由于
String
类是不可变的,所以对字符串进行修改时需要创建新的字符串对象,这会带来一些性能开销。而StringBuilder
和StringBuffer
类都可以在原地修改字符串的内容,因此它们在处理大量字符串修改操作时性能更优。线程安全:
StringBuffer
类是线程安全的,它的所有方法都是同步的,可以在多线程环境下安全地使用。而StringBuilder
类不是线程安全的,它的方法不是同步的,在多线程环境下使用时需要注意同步问题。
总之,String
、StringBuilder
和StringBuffer
都是Java中表示字符串的类,它们各有优缺点和适用场景。在使用时,应根据具体需求选择合适的类。
# 异常处理
# 异常体系
Java中的异常体系是一个分层的类结构,其中所有的异常类都继承自Throwable
类。Throwable
类有两个重要的子类:Error
和Exception
。
Error:
Error
类及其子类表示严重的错误,通常是由Java运行时系统内部错误或资源耗尽引起的。这些错误通常无法被程序处理,应用程序应该尽量避免这些错误的发生。Exception:
Exception
类及其子类表示程序运行过程中可能发生的异常情况。这些异常可以被程序捕获并处理。Exception
类又分为两种类型:受检异常(checked exception)和非受检异常(unchecked exception)。受检异常:受检异常是指那些必须在编译时进行处理的异常。如果一个方法可能抛出受检异常,那么它必须在方法声明中使用
throws
关键字来声明这些异常,调用该方法的代码则必须捕获这些异常或者再次声明抛出这些异常。非受检异常:非受检异常是指那些不需要在编译时进行处理的异常。它们通常表示程序逻辑错误,如空指针异常、数组越界异常等。非受检异常包括
RuntimeException
及其子类和Error
及其子类。
总之,Java中的异常体系是一个分层的类结构,其中包括了各种不同类型的异常。程序员应该根据具体情况选择合适的异常类型来处理程序运行过程中可能发生的错误和异常情况。
# I/O
# I/O流的分类
Java中的IO流可以根据不同的维度进行分类。
按照流的方向:可以将IO流分为输入流和输出流。输入流用于从数据源读取数据,输出流用于向数据目的地写入数据。
按照处理数据的单位:可以将IO流分为字节流和字符流。字节流用于处理字节数据,字符流用于处理字符数据。
按照流的功能:可以将IO流分为节点流和处理流。节点流用于直接操作数据源或数据目的地,处理流用于在节点流的基础上增加额外的功能。
根据上述分类方法,Java中常用的IO流类可以分为以下几类:
字节输入流:
InputStream
是所有字节输入流的抽象基类,它定义了读取字节数据的基本方法。常用的子类有FileInputStream
、ByteArrayInputStream
等。字节输出流:
OutputStream
是所有字节输出流的抽象基类,它定义了写入字节数据的基本方法。常用的子类有FileOutputStream
、ByteArrayOutputStream
等。字符输入流:
Reader
是所有字符输入流的抽象基类,它定义了读取字符数据的基本方法。常用的子类有FileReader
、StringReader
等。字符输出流:
Writer
是所有字符输出流的抽象基类,它定义了写入字符数据的基本方法。常用的子类有FileWriter
、StringWriter
等。处理流:处理流是在节点流的基础上增加额外功能的流。常用的处理流有缓冲流(如
BufferedInputStream
、BufferedOutputStream
、BufferedReader
、BufferedWriter
等)、转换流(如InputStreamReader
、OutputStreamWriter
等)、对象流(如ObjectInputStream
、ObjectOutputStream
等)等。
总之,Java中的IO流可以根据不同维度进行分类,包括按照方向、按照处理数据单位和按照功能等。不同类型的IO流提供了不同的功能,可以根据具体需求选择合适的IO流来进行输入输出操作。
# 对BIO、NIO、AIO的理解
# 序列化
# 什么是序列化、反序列化
# 几种常用的序列化、反序列化方式
# 泛型
# 为什么要有泛型
泛型(Generics)是一种编程语言的特性,它允许程序员在定义类、接口和方法时使用类型参数。类型参数可以在实例化或调用时指定具体的类型,从而提供了更强的类型检查和更好的代码重用性。
泛型的主要优点有以下几点:
类型安全:泛型提供了更强的类型检查,可以在编译时检测出潜在的类型转换错误。例如,在使用泛型容器类时,可以指定容器中元素的类型,从而避免在运行时出现类型转换错误。
代码重用:泛型允许程序员定义通用的类、接口和方法,这些类、接口和方法可以用于不同的数据类型。例如,可以定义一个通用的排序方法,该方法可以用于对整数、字符串和其他数据类型进行排序。
可读性:泛型提高了代码的可读性,使程序员能够更清楚地表达他们的意图。例如,在使用泛型容器类时,可以指定容器中元素的类型,从而使代码更容易理解。
总之,泛型提供了更强的类型检查、更好的代码重用性和更高的可读性,是一种非常有用的编程语言特性。
下面是一个简单的 Java 泛型示例,它演示了如何使用泛型定义一个简单的容器类:
public class Container<T> {
private T value;
public Container(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在上面的代码中,我们定义了一个名为 Container
的泛型类,它包含一个类型参数 T
。类型参数 T
可以在实例化 Container
类时指定具体的类型。
下面是一个使用 Container
类的示例:
Container<Integer> intContainer = new Container<>(1);
int intValue = intContainer.getValue(); // intValue = 1
Container<String> stringContainer = new Container<>("Hello");
String stringValue = stringContainer.getValue(); // stringValue = "Hello"
2
3
4
5
在上面的代码中,我们分别创建了两个 Container
类的实例:一个用于存储整数,另一个用于存储字符串。由于我们在实例化 Container
类时指定了具体的类型参数,因此编译器能够对类型进行检查,避免出现类型转换错误。
这个简单的示例演示了如何使用泛型定义和使用通用的类。通过使用泛型,我们可以提高代码的类型安全性、重用性和可读性。
# 注解
# 为什么要有注解啊
一是标记,我们对注解标记的类/属性/方法进行对应的处理; 二是注解本身有一些信息,可以参与到处理的逻辑中。 注解(Annotation)是一种编程语言的特性,它允许程序员在源代码中添加一些元数据(Metadata),用于描述或标记代码的含义、功能或行为。注解可以在编译时或运行时被处理,从而实现一些额外的功能。
注解的主要优点有以下几点:
简化配置:注解可以用来替代一些繁琐的配置文件,使配置更加简洁和直观。例如,在使用 Spring 框架时,可以使用注解来配置 Bean 的依赖注入、事务管理、切面编程等功能。
生成文档:注解可以用来生成文档,使文档更加规范和完整。例如,在使用 Javadoc 工具时,可以使用注解来描述类、方法、参数等的含义和作用。
代码分析:注解可以用来进行代码分析,使代码更加健壮和高效。例如,在使用 Lombok 工具时,可以使用注解来自动生成 Getter、Setter、ToString 等方法。
注解确实可以用作标记代码的含义、功能或行为。它允许程序员在源代码中添加一些元数据(Metadata),用于描述或标记代码的含义、功能或行为。这些元数据可以在编译时或运行时被处理,从而实现一些额外的功能。
总之,注解提供了一种灵活和强大的方式来描述或标记代码的含义、功能或行为,是一种非常有用的编程语言特性。
# 什么是反射?应用场景?反射原理?
# Java8新特性
- lambda表达式
- Stream流
- Optional类
- 日期类