一、基础
1、聊一聊Java平台的理解!
2、面向对象
实现多态,需要满足3个必要条件:
● 继承:多态发生在继承关系中,必须存在有继承关系的父类和子类中,多态建立在封装和继承的基础之上;
● 重写:必须要有方法的重写,子类对父类的某些方法重新定义;
● 向上转型:就是要将父类引用指向子类对象,只有这样该引用才既能调用父类的方法,又能调用子类的方法。
多态的实现有如下几种方式:
● 方法重载:重载可以根据实际参数的数据类型、个数和次序,在编译时确定执行重载方法中的哪一个。
● 方法重写:这种方式是基于方法重写来实现的多态;
● 接口实现:接口是一种无法被实例化但可以被实现的抽象类型,是对抽象方法的集合。定义一个接口可以有多个实现,这也是多态的一种实现形式,与继承中方法的重写类似。
1.Java中的基本数据类型有哪些?有什么区别?
Java中的基本数据类型(也称为原始数据类型)是用于存储简单值的数据类型,而不是对象。Java中共有八种基本数据类型: 1个字节=8bit位
byte:1字节,范围是-128到127。用于表示整数值,通常用于节省内存的情况,例如图像处理 等。
short:2字节,范围是-32,768到32,767。用于表示整数值,通常用于节省内存的情况。
int:4字节,范围是-2^31到2^31-1。用于表示整数值,是最常用的整数类型。
long:8字节,范围是-2^63到2^63-1。用于表示较大范围的整数值,当int类型不足以表示时使用。
float:4字节,范围是IEEE 754单精度浮点数。用于表示带有小数部分的数值。
double:8字节,范围是IEEE 754双精度浮点数。用于表示更高精度的数值,通常用于科学计算和金融领域。
char:2字节,表示Unicode字符,范围是0到65535。用于表示单个字符。
boolean:boolean
类型在 Java 中表示逻辑布尔值,通常在内存中使用一个字节表示,其本质是一个简单的二进制标志位,用来表示 true
或 false
。
布尔类型通常被定义为占用一个字节的内存空间。这是因为在计算机中,最小的可寻址内存单元是字节(8位)。虽然一个比特(bit)可以表示逻辑上的真(true)或假(false),但计算机处理数据时需要将其存储在地址上可寻址的单位内。
因此,即使布尔类型的值只需一个比特来表示真或假,计算机为了对齐和存储方便,通常将布尔类型存储为一个字节,也就是8位,这样可以更高效地管理内存。
区别:
大小:基本数据类型的大小在内存中是固定的,不受Java虚拟机的实现和平台的影响。
存储位置:基本数据类型的值直接存储在栈(Stack)中,而不是存储在堆(Heap)中。这使得基本数据类型的访问速度较快,但也限制了其作用域。
值传递:基本数据类型在方法传参时是按值传递的,即将实际值传递给方法的参数,方法内部的修改不会影响原始值。
默认值:如果在方法或类中声明了一个基本数据类型的变量,但没有初始化它,它将自动被赋予其对应的默认值:0(对于数值类型)、false(对于boolean类型)和'\u0000'(对于char类型)。
不支持null:基本数据类型不支持null值,因为它们不是对象。如果需要表示缺失或未初始化状态,可以使用对应的包装类(如Integer、Double等)。
数学运算:对于基本数据类型,可以进行常规的数学运算,例如加减乘除。而包装类对象不能直接进行数学运算,需要通过拆箱(Unboxing)将其转换为基本数据类型后才能进行运算。
数组等),它们都是基于基本数据类型和对象引用构建的。
2.什么是Java中的装箱和拆箱?
-
基本数据类型(Primitive Data Types):
- 包括
byte
、short
、int
、long
、float
、double
、char
和boolean
等8种类型。 - 基本数据类型在内存中直接存储值,占用固定的内存空间,并且具有较高的性能。
- 不能调用方法,没有成员变量,没有对象特性。
- 在栈上分配内存空间。
- 包括
-
包装类型(Wrapper Classes):
- Java为每种基本数据类型提供了对应的包装类,如
Byte
、Short
、Integer
、Long
、Float
、Double
、Character
和Boolean
。 - 包装类型是对象,具有成员变量和方法,可以像普通对象一样进行操作,例如调用方法、存储在集合中等。
- 提供了一些有用的方法,如类型转换、数值比较、字符串转换等。
- 在堆上分配内存空间。
- Java为每种基本数据类型提供了对应的包装类,如
装箱(Boxing)和拆箱(Unboxing)是Java中用于在基本数据类型和对应的包装类之间进行转换的概念。
- 装箱(Boxing): 装箱是将基本数据类型转换为对应的包装类对象的过程。在Java中,为每种基本数据类型都定义了对应的包装类,这些包装类位于
java.lang
包下,如Integer
对应int
,Double
对应double
,Boolean
对应boolean
等。当我们需要将基本数据类型的值存储在对象中,或者需要将基本数据类型传递给一个接受对象参数的方法时,就需要进行装箱操作。
示例:
int num = 42;
Integer integerObj = Integer.valueOf(num); // 装箱,将int转换为Integer对象
Java 5之后引入了自动装箱(Autoboxing)的特性,使得装箱操作更加方便。自动装箱允许我们直接将基本数据类型赋值给对应的包装类对象,Java会自动进行装箱的转换。
示例:
int num = 42; Integer integerObj = num; // 自动装箱,无需调用valueOf方法
- 拆箱(Unboxing): 拆箱是将包装类对象转换为对应的基本数据类型的过程。当我们需要从一个包装类对象中获取其对应的基本数据类型的值时,就需要进行拆箱操作。
示例:
Integer integerObj = 100; int num = integerObj.intValue(); // 拆箱,将Integer对象转换为int
Java 5之后引入了自动拆箱(Unboxing)的特性,使得拆箱操作更加方便。自动拆箱允许我们直接将包装类对象赋值给对应的基本数据类型变量,Java会自动进行拆箱的转换。
示例:
Integer integerObj = 100; int num = integerObj; // 自动拆箱,无需调用intValue方法
使用包装类型的主要优势之一是可以将基本数据类型放入集合类(如List、Map等)中,因为集合类只能存储对象而不是基本数据类型。此外,包装类型还能够在泛型类和方法中使用,以及处理与对象相关的操作,如调用方法和处理异常。然而,由于包装类型是对象,因此相比于基本数据类型,其内存开销和性能略有降低。
需要注意的是,装箱和拆箱可能会导致性能损失,因为它涉及到对象的创建和销毁。在进行大量数据操作时,应当谨慎使用装箱和拆箱,以避免不必要的性能开销。
3.Java中的四种访问修饰符是什么?它们分别表示什么意思?
Java中有四种访问修饰符,用于控制类、方法、变量以及构造方法的访问权限。这些访问修饰符分别是:
-
public(公共访问修饰符):
- 表示被修饰的类、方法、变量或构造方法可以在任何地方被访问,即没有访问限制。
- 在不同的包(package)中也可以被访问,因为它具有最宽松的访问权限。
-
protected(受保护访问修饰符):
- 表示被修饰的方法、变量或构造方法可以在同一个包内的其他类中被访问,也可以在不同包中的子类中被访问。
- 在不同包中的非子类中,也不能直接访问protected成员。
-
default(默认访问修饰符):
- 在Java中没有显式写明访问修饰符的情况下,默认的访问修饰符就是default。
- 表示被修饰的类、方法、变量或构造方法只能在同一个包中被访问,不能在不同包中被访问。
-
private(私有访问修饰符):
- 表示被修饰的方法、变量或构造方法只能在同一个类中被访问,其他类无法访问。
- 这是最严格的访问权限,主要用于隐藏类的实现细节,保护数据安全性。
这些访问修饰符的范围从最宽松到最严格依次是:public > protected > default > private。要根据不同的需求来选择合适的访问修饰符。
注意:访问修饰符只能用于类的成员(字段、方法、内部类等),不能用于局部变量
4.Java中的final关键字有什么作用?
在Java中,final
是一个关键字,用于表示不可变的常量、不可继承的类、不可重写的方法以及保护对象引用不被修改。它可以用于修饰类、方法和变量。final
关键字的作用如下:
- 不可变常量:当
final
用于修饰变量时,表示该变量是一个常量,一旦被赋值后,其值就不能再改变。常量在声明时必须进行初始化,并且不能再次赋值。
final int MAX_COUNT = 100;
final double PI = 3.14159;
final String GREETING = "Hello";
- 不可继承的类:当
final
用于修饰类时,表示该类是不可继承的,即不能被其他类继承。
final class MyClass { // 类的内容 }
- 不可重写的方法:当
final
用于修饰方法时,表示该方法是不可重写的,即子类不能对该方法进行覆盖。
class ParentClass { final void finalMethod() { // 方法的内容 } } class ChildClass extends ParentClass { // 下面的代码将会引发编译错误,因为无法重写finalMethod() // void finalMethod() { } }
- 保护对象引用不被修改:当
final
用于修饰对象引用时,表示该引用指向的对象地址不能被修改,但是对象本身的内容是可以修改的。
final int[] array = {1, 2, 3};
array[0] = 10; // 合法,修改数组元素的值
// array = new int[]{4, 5, 6}; // 非法,不能修改数组引用的地址
使用final
关键字可以增加代码的可读性和安全性。对于常量和不可继承的类,可以避免不必要的改动和错误;对于不可重写的方法,可以确保方法的行为不被子类修改;对于对象引用,可以避免引用被意外修改。
5.什么是静态变量和静态方法?它们如何使用?
静态变量和静态方法是与类相关联而不是与类的实例相关联的成员。它们属于类本身而不是类的每个对象。以下是关于静态变量和静态方法的详细说明以及如何使用它们:
静态变量(Static Variables):
静态变量也被称为类变量,它们用 static
关键字声明。这些变量在类的加载过程中被分配内存空间,只会有一份拷贝存在于内存中,无论类被实例化多少次。静态变量通常用于存储与类相关的常量或共享数据。
示例:
public class MyClass {
// 静态变量
public static int count = 0;
}
静态方法(Static Methods):
静态方法也用 static
关键字声明,它们不依赖于类的实例,可以直接通过类名来调用。静态方法通常用于实现与类相关的实用函数,它们无法访问类的非静态成员(实例变量和实例方法),因为它们没有特定的实例上下文。
示例:
public class MathUtils {
// 静态方法
public static int add(int a, int b) {
return a + b;
}
}
如何使用静态变量和静态方法:
-
访问静态变量:可以使用类名来访问静态变量,例如
MyClass.count
。 -
调用静态方法:可以使用类名来调用静态方法,例如
MathUtils.add(2, 3)
。 -
共享数据:静态变量适用于存储多个对象之间共享的数据,例如计数器、配置信息等。
-
实用函数:静态方法通常用于实现与类相关的实用函数,例如数学运算、日期处理等。
需要注意的是,静态变量和静态方法在内存中只有一份拷贝,因此它们是共享的。但要小心在多线程环境下使用静态变量,可能需要考虑线程安全性,例如使用 synchronized
或其他同步机制。静态方法无法访问非静态成员,因此在静态方法中不能使用 this
关键字来引用当前对象。
6.Java中的抽象类和接口有什么区别?何时使用它们?
抽象类(Abstract Class)和接口(Interface)是 Java 中用于实现抽象类型的两种不同方式,它们有一些关键区别,适用于不同的编程场景。以下是抽象类和接口的主要区别以及何时使用它们的建议:
抽象类(Abstract Class):
-
定义:抽象类是一个类,可以包含抽象方法(方法没有实现),也可以包含具体方法(有实现),并且可以包含成员变量。
-
继承:子类必须使用
extends
关键字继承抽象类,并且只能继承一个抽象类。 -
构造方法:抽象类可以有构造方法,子类必须调用父类的构造方法。
-
访问修饰符:抽象类的方法可以有各种访问修饰符,包括
public
、protected
、private
等。 -
变量:抽象类可以包含成员变量,可以有实例变量和静态变量。
接口(Interface):
-
定义:接口是一种特殊的类,它只包含抽象方法和常量(
public static final
变量),不包含具体实现。 -
实现:类通过
implements
关键字来实现接口,一个类可以实现多个接口。 -
构造方法:接口不能包含构造方法,因为接口不能被实例化。
-
访问修饰符:接口中的方法默认都是
public
,并且不允许使用其他修饰符。 -
变量:接口中只能包含常量,即
public static final
变量。
何时使用抽象类和接口:
-
抽象类的使用:
- 当你想要创建一个类的模板,其中包含一些通用的方法实现,并且这些方法对于所有子类都是一样的时,可以使用抽象类。
- 当你希望限制一个类只能被单一继承时,可以使用抽象类,因为 Java 不支持多重继承。
- 当你需要在类中包含成员变量时,抽象类是一个更好的选择,因为接口只能包含常量。
-
接口的使用:
- 当你需要定义一组方法的规范,但不需要提供方法的具体实现时,可以使用接口。
- 当一个类需要实现多个不相关的类型时,可以使用接口,因为 Java 支持多个接口的实现。
- 当你希望实现多态性,通过接口可以使不同类实现同一接口并以不同方式实现接口中的方法。
通常来说,如果你只需要定义方法的规范而不关心实现,或者你希望实现多态性,那么使用接口是一个更好的选择。如果你有一些通用的方法实现,或者希望限制类的继承关系,那么使用抽象类可能更合适。实际项目中,你可能会同时使用抽象类和接口,以满足不同的需求。
7.什么是多态性?如何在Java中实现多态?
多态性(Polymorphism)是面向对象编程的一个重要概念,它指的是同一种操作或方法可以在不同的对象上产生不同的行为。多态性使得一个类的实例可以被视为其父类的实例,从而实现代码的通用性和灵活性。多态性有两种主要形式:编译时多态性和运行时多态性。
编译时多态性:
编译时多态性是指在编译时期确定方法或操作的调用,它通常与方法重载(Overloading)相关。编译器会根据方法参数的数量和类型来选择调用哪个方法。这种多态性在编译时已经确定,不涉及到运行时的动态绑定。
示例:
public class PolymorphismExample {
public static void main(String[] args) {
int result1 = add(2, 3); // 调用int类型参数的add方法
double result2 = add(2.5, 3.7); // 调用double类型参数的add方法
}
public static int add(int a, int b) {
return a + b;
}
public static double add(double a, double b) {
return a + b;
}
}
运行时多态性(动态多态性):
运行时多态性是指在运行时期确定方法或操作的调用,它通常与方法重写(Overriding)和接口的实现相关。在运行时,对象的类型会被检查,然后调用相应的方法。这种多态性允许一个对象根据其实际类型来执行不同的行为。
示例:
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat meows");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal myPet = new Dog();
myPet.makeSound(); // 调用Dog类的makeSound方法
myPet = new Cat();
myPet.makeSound(); // 调用Cat类的makeSound方法
}
}
在上面的示例中,Animal
类有一个 makeSound
方法,而 Dog
和 Cat
类都重写了这个方法。通过多态性,可以在运行时根据对象的实际类型来调用适当的方法。
要实现运行时多态性,需要满足以下条件:
- 必须存在继承关系或实现了相同接口的类。
- 子类必须重写父类或接口中的方法。
- 使用父类或接口类型的引用来引用子类的对象。
多态性提高了代码的可扩展性和灵活性,使得在不修改现有代码的情况下可以添加新的子类或实现新的接口。这是面向对象编程中一个非常重要的概念。
8.Java中的异常处理机制是什么?列举几个常见的异常类。
9.如何在Java中创建线程?有几种方式?
在Java中,可以使用多种方式创建线程。主要有以下四种方式:
使用Thread类:通过继承Thread类,并重写其run()方法来创建线程。然后通过调用start()方法启动线程。
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码逻辑
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
使用Runnable接口:通过实现Runnable接口,并实现其run()方法来创建线程。然后通过创建Thread对象并传入Runnable对象,调用start()方法启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码逻辑
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
使用Callable和Future:Callable是一个带有泛型的接口,可以通过实现它的call()方法来创建线程。它可以返回一个结果,并且可以通过Future接口来获取线程执行的结果。
import java.util.concurrent.*;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程执行的代码逻辑
return "Thread result";
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
String result = future.get();
System.out.println(result);
executor.shutdown();
}
}
使用Executor框架:Executor框架是Java 5引入的并发执行框架,它可以通过线程池来管理线程的生命周期,避免频繁创建和销毁线程,提高性能。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Runnable() {
@Override
public void run() {
// 线程执行的代码逻辑
}
});
executor.shutdown();
}
}
以上四种方式都可以创建线程,使用Runnable接口和Executor框架是更加常见和推荐的方式。使用Runnable接口可以避免类继承的限制,而Executor框架可以更好地管理线程,提高线程的重用性和执行效率。使用Callable和Future接口可以获得线程的执行结果,并处理异常情况。
10.Java中的垃圾回收是如何工作的?
11.什么是Java中的泛型?如何使用泛型?
12.请解释Java中的反射是什么,并提供一个使用反射的示例。
13.Java中的equals()和hashCode()方法有什么作用?它们之间有什么关联?
14.Java中的集合框架有哪些?它们之间有什么区别?
15.Java中的序列化是什么?如何实现对象的序列化和反序列化?
16.什么是Lambda表达式?它在Java中的使用场景是什么?
17.Java中的Stream API是什么?它有什么优势?
18.什么是线程安全?如何在Java中实现线程安全?
19.Java中的注解是什么?你可以举例说明一些常见的注解用途吗?
20.请解释Java中的类加载器是什么?它有哪些类型?
在Java中,类加载器(Class Loader)是Java虚拟机(JVM)的一部分,负责将类的字节码从各种来源加载到JVM中,并将其转换为可执行的Java类。类加载器在Java应用程序的运行时环境中起着重要作用,它使得Java的动态性和灵活性成为可能,允许在运行时动态加载和卸载类。
类加载器的主要任务包括以下几点:
-
加载(Loading):查找并加载类的字节码数据。类加载器将字节码数据加载到JVM中的内存中,以便后续的链接、验证和初始化。
-
链接(Linking):将已加载的类与其它类和资源进行连接。链接的过程包括验证类的字节码,解析类和接口的符号引用,以及准备类中的静态变量。
-
初始化(Initialization):对类进行初始化,执行类的静态初始化块和赋初值操作。
-
卸载(Unloading):在某些情况下,当类不再被引用时,类加载器可以卸载已加载的类以释放内存。
Java中的类加载器可以分为以下几种类型:
-
引导类加载器(Bootstrap Class Loader):它是JVM的一部分,负责加载Java核心类库,如
java.lang
包中的类。这些类在JVM启动时就被加载,无法通过普通的Java代码获取其引用。 -
扩展类加载器(Extension Class Loader):负责加载Java的扩展类库,位于
jre/lib/ext
目录下的JAR文件。这些类库是为了扩展JDK功能而提供的。 -
应用程序类加载器(Application Class Loader):也称为系统类加载器,负责加载应用程序类路径上的类。这是大多数Java应用程序默认使用的类加载器。
-
自定义类加载器(Custom Class Loader):开发人员可以根据需要创建自定义的类加载器,用于加载自定义位置的类或实现特定的类加载策略。
类加载器在Java中的工作方式是通过类的双亲委派模型(Parent-Delegation Model)来实现的。当一个类加载器被要求加载一个类时,它首先会将这个任务委托给其父类加载器。如果父加载器无法加载这个类,子加载器才会尝试加载。这种层次结构保证了类的一致性和避免了类的重复加载。
类加载器的正确使用对于Java应用程序的性能和安全性至关重要。不同的类加载器可以为不同的类提供隔离的命名空间,从而实现类的隔离和保护。
21.匿名内部类的详细介绍和用法
匿名内部类是Java编程语言中的一个特性,它允许你在创建对象的同时定义一个类,而无需显式地为该类命名。匿名内部类通常用于创建实现某个接口或继承某个类的对象,以便在需要时直接使用,而不必单独定义一个新的类。这在某些情况下可以使代码更简洁,但也可能使代码结构变得复杂不易阅读。
匿名内部类的一般语法形式如下:
new 父类名/接口名() {
// 实现/重写父类的方法或接口的方法 // 可以定义成员变量和方法
}
下面是一个匿名内部类的简单示例,假设有一个接口 Runnable
:
public interface Runnable {
void run();
}
使用匿名内部类创建一个 Runnable
对象:
public class Main {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello from anonymous inner class!");
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
匿名内部类的使用场景:
-
事件处理程序:在图形用户界面(GUI)编程中,可以使用匿名内部类来处理按钮点击、鼠标事件等。
-
接口回调:当你需要实现一个接口的方法,但只需要在特定的上下文中使用它时,匿名内部类可以用来实现这些接口方法。
-
单次使用类:如果一个类只需要在一个地方使用一次,并且不需要进行复用,可以考虑使用匿名内部类来避免创建一个额外的类。
-
简化代码:当你只需要少量的代码来实现某个功能时,使用匿名内部类可以减少冗余。
-
继承抽象类:匿名内部类也可以继承抽象类,并实现其中的抽象方法。
尽管匿名内部类可以使代码更紧凑,但它也有一些限制和潜在的缺点:
-
只能实例化一次:匿名内部类只能用于创建一个实例,无法在其他地方重复使用。
-
不易阅读:过多的匿名内部类可能会使代码变得难以理解和维护,因为无法直观地看出类的结构和用途。
-
访问外部变量:匿名内部类可以访问外部类的成员变量,但必须声明为
final
或事实上的final
(即不再修改)。
总的来说,匿名内部类在一些特定场景下可以提供方便,但需要在代码的可读性和维护性之间做权衡。在某些情况下,建议使用命名的局部类或独立的类来代替匿名内部类,以提高代码的可读性和可维护性。