Java中的Exception和Error

Exception和Error都是Throwable类的子类(Java中只有继承了Throwable类的实例才能被throw和catch)。

总结来看:
Exception表示可以被处理的程序异常,Error表示系统及的不可恢复错误。

详细说明:

  1. Exception:是程序中可以处理的异常情况,表示程序逻辑或外部环境中的问题,可以通过代码进行恢复或处理。
    IOExceptionSQLExceptionNullPointerException

Exception又分为Checked Exception(编译期异常)和Unchecked Exception(运行时异常)

  • Checked Exception:在编译时必须显式处理(如使用 try-catch
    抛出)。如 IOException。
  • Unchecked Exception:运行时异常,不需要显式捕获。常见的如
    IllegalArgumentException 等,继承自
  1. Error:表示严重的错误,通常是JVM层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。例如内存耗尽(OutOfMemoryError)、栈溢出
    (StackOverflowError) o

Error不应该被程序捕获或处理,因为一般出现这种错误时程序无法继续运行。

Java的多态

概念

多态是指同一个接口或父类引用变量可以指向不同的对象实例,并根据实际指向的对象类型执行相应的方法。

它允许同一方法在不同对象上表现出不同的行为,是面向对象编程(OOP)的核心特性之一。

多态的优点:

  • 通过多态,程序可以灵活地处理不同类型的对象,降低代码耦合度,增强系统的可扩展性。新增子类或实现类时,无需修改原有代码,只需通过接口或父类引用调用即可。


多态其实是一种抽象行为。

举个例子:
一个人可以说学生,也可以是老师。

我们定义Persion类,用student类和teacher类分别继承它。

Persion类含一个方法work()。

那么这时,student类内重写work()为上课,teacher类内重写work()为教书。

使用时,对象都是persion,但new不同实现类,表现形式不同,这就是多态。

编译时多态和运行时多态

编译时多态和运行时多态是面向对象编程中多态性的两种实现方式,它们分别在不同的阶段决定方法的绑定。

  • 编译时多态:通过方法重载实现,在编译时确定方法的调用。
  • 运行时多态:通过方法重写实现,在运行时确定方法的调用。
  1. 编译时多态,也称为静态多态,是在编译阶段确定方法的调用。编译时多态主要通过方法重载(Method Overloading)实现。

    方法重载;指在同一个类中定义多个方法,这些方法的名称相同但参数列表(参数的类型或数量)不同。Java编译器在编译时会根据方法调用时传入的参数类型和数量,决定调用哪一个重载方法。

  2. 运行时多态,也称为动态多态,是在运行时确定方法的调用。运行时多态通过方法重写(Method Overriding) 实现。

    方法重写:子类重写父类的一个或多个方法。通过父类引用调用方法时,实际执行的是子类重写后的方法。这种多态性是在运行时根据对象的实际类型决定的。

Java中的参数传递是按值还是按引用?

在Java中,参数传递只有按值传递,无论是基本类型还是引用类型。

  • 基本类型:如int、float等,存储在栈内存中,方法中对基本参数的操作只会影响传递的副本,原始变量的值不受影响。
  • 引用类型:包括所有的对象和数组,引用类型的变量存储是对象在堆内存中的地址。传递时是地址的副本。方法内修改可以影响到传入对象的内容,但不会影响对象本身的地址。

Java方法重载和方法重写的区别

区别 重载 重写
发生场景 同一个类中 继承关系的子父类之间
参数列表 必须不同(参数数量、类型、顺序) 必须相同
返回类型 可以不同 必须与父类方法的返回类型相同,或是父类返回类型的子类
访问修饰符 不受影响 相同或更广
静态方法和非静态方法 都可以 只能重写非静态方法,(非静态方法可以被隐藏)
异常处理 可以不同 子类异常不能抛出比父类更多的异常

Java内部类

Java 内部类是指在一个类的内部定义的类,Java支持多种类型的内部类,包括成员内部类、局部内部类、匿名内部类和静态内部类。内部类可以访问外部类的成员变量和方法,甚至包括私有的成员。

内部类的作用:

  1. 封装性:将逻辑相关的类封装在一起,提高类的内聚性。
  2. 访问外部类成员:内部类可以方便地访问外部类的成员变量和方法,尤其在需要操作外部类对象的场景下非常有用。
  3. 简化代码:对于只在一个地方使用的小类,内部类能减少冗余代码,简化结构。
  4. 事件处理:匿名内部类广泛用于实现回调函数或事件监听,简化了代码结构,特别是对于实现接口或抽象类的场景。

内部类类型:

  • 成员内部类:非静态类,作为外部类的一个成员。它可以直接访问外部类的所有成员,包括私有成员。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class OuterClass {
    private String outerField = "OuterField";

    class InnerClass {
    void display() {
    System.out.println("OuterField: " + outerField);
    }
    }

    public void createInner() {
    InnerClass inner = new InnerClass();
    inner.display();
    }
    }
  • 静态内部类:定义为static,无法访问外部类的非静态成员,只能访问外部类的静态成员。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class OuterClass {
    private static String staticOuterField = "Static Outer Field";

    static class StaticInnerClass {
    void display() {
    System.out.println("Static Outer Field: " + staticOuterField);
    }
    }

    public static void createStaticInner() {
    StaticInnerClass staticInner = new StaticInnerClass();
    staticInner.display();
    }
    }
  • 局部内部类:定义在方法或代码块中的类,仅在该方法或代码块内可见,通常用于临时的对象构建。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class OuterClass {
    void outerMethod() {
    final String localVar = "Local Variable";

    class LocalInnerClass {
    void display() {
    System.out.println("Local Variable: " + localVar);
    }
    }

    LocalInnerClass localInner = new LocalInnerClass();
    localInner.display();
    }
    }
  • 匿名内部类:没有类名的内部类,通常用于创建短期使用的类实例,尤其是在接口回调或事件处理时被广泛使用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class OuterClass {
    interface Greeting {
    void greet();
    }

    public void sayHello() {
    Greeting greeting = new Greeting() {
    @Override
    public void greet() {
    System.out.println("Hello, World!");
    }
    };
    greeting.greet();
    }
    }

Java中String、StringBuffer、StringBuilder的区别

  1. String
    • 不可变:String是不可变类,每次修改都会创建新的String对象。
    • 适合场景:用于字符串不会频繁变化的场景,例如少量的字符串拼接操作或字符串常量。
  2. StringBuffer
    • 可变:String是可变的,可以进行字符串的追加、删除、插入等。
    • 线程安全:内部使用synchornized关键字来保证多线程下的安全性。
    • 适合场景:用于多线程环境中需要频繁修改字符串的场景。
  3. StringBuilder
    • 可变:提供了与StringBuffer类似的操作接口。
    • 非线程安全:不保证线程安全,性能比StringBuffer更高。
    • 适合场景:适用于单线程环境下需要大量修改字符串的场景,如高频拼接操作。

总结

  • String:不可变,适用少量字符串操作。
  • StringBuffer:可变且线程安全,适合多线程环境中的频繁字符串修改。
  • StringBuilder:可变且非线程安全,适合单线程环境中的高性能字符串处理。

什么是Java中的动态代理?

Java中的动态代理是一种在运行时创建代理对象的机制。动态代理允许程序在运行时决定代理对象的行为,而不需要在编译时确定。它通过代理模式为对象提供一种机制,使得可以在不修改目标对象的情况下对其进行增强或调整。

代理可以看作是调用目标的一个包装,通常用来调用真实的目标之前进行一些逻辑处理,消除一些重复代码。

静态代理指的是我们预先编码好一个代理类,而动态代理指的是运行时生成代理类。

动态代理主要用途

  • 简化代码:通过代理模式,可以减少重复代码,尤其是在横切关注点(如日志记录、事务管理、权限控制等)方面。
  • 增强灵活性:动态代理使得代码更具灵活性和可扩展性,因为代理对象是在运行时生成的,可以动态地改变行为。
  • 实现AOP:动态代理是实现面向切面编程(AOP,Aspect-Oriented Programming)的基础,可以在方法调用前后插入额外的逻辑。

Java 动态代理与CGLIB代理:

  • Java 动态代理:只能对接口进行代理,不支持对类进行代理。
  • CGLIB代理:通过字节码技术动态生成目标类的子类来实现代理,支持对类(非接口)进行代理。

JDK代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 目标接口
interface MyService {
void doSomething();
}

// 目标对象的实现
class MyServiceImpl implements MyService {
@Override
public void doSomething() {
System.out.println("Doing something...");
}
}

// 动态代理处理器
class MyInvocationHandler implements InvocationHandler {
private final Object target;

public MyInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method call");
Object result = method.invoke(target, args);
System.out.println("After method call");
return result;
}
}

// 使用动态代理
public class Main {
public static void main(String[] args) {
MyService target = new MyServiceImpl();
MyService proxy = (MyService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new MyInvocationHandler(target)
);

proxy.doSomething();
}
}

CGLIB代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

// 目标类
class MyService {
public void doSomething() {
System.out.println("Doing something...");
}
}

// CGLIB 代理处理器
class MyInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method call");
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method call");
return result;
}
}

// 使用 CGLIB 动态代理
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
enhancer.setCallback(new MyInterceptor());

MyService proxy = (MyService) enhancer.create();
proxy.doSomething();
}
}

Java中注解的原理

注解其实就是一个标记,是一种提供元数据的机制,用于给代码添加说明信息。可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。

注解本身不影响程序的逻辑执行,但可以通过工具或框架来利用这些信息进行特定的处理,如代码生成、编译时检查、运行时处理等。

Java中的反射

Java的反射机制是指在运行时获取类的结构信息(如方法、字段、构造函数)并操作对象的一种机制。反射机制提供了在运行时动态创建对象、调用方法、访问字段等功能,而无需在编译时知道这些类的具体信息。

反射机制的优点:

  • 可以动态地获取类的信息,不需要在编译时就知道类的信息。
  • 可以动态地创建对象,不需要在编译时就知道对象的类型。
    8 可以动态地调用对象的属性和方法,在运行时动态地改变对象的行为。

一般在业务编码中不会用到反射,在框架上用的较多,因为很多场景需要很灵活,不确定目标对象的类型,届时只能通过反射动态获取对象信息。

例如Spring使用反射机制来读取和解析配置文件,从而实现依赖注入和面向切面编程等功能,

Java中的SPI机制

SPI:Service Provider Interface

SPI是一种插件机制,用于在运行时动态加载服务的实现。它通过定义接口(服务接口)并提供一种可扩展的方式来让服务的提供者(实现类)在运行时注入,实现解耦和模块化设计。

SPI 机制的核心概念:

  • 服务接口:接口或抽象类,定义某个服务的规范或功能。
  • 服务提供者:实现了服务接口的具体实现类。
  • 服务加载器(ServiceLoader):Java提供的工具类,负责动态加载服务的实现类。通过ServiceLoader可以在运行时发现和加载多个服务提供者。
  • 配置文件:服务提供者通过在META-INF/services/目录下配置服务接口的文件来声明自己。这些文件的内容是实现该接口的类的完全限定名。

SPI 机制的优势:

  • 解耦:接口与实现分离,客户端不需要依赖具体实现,能够在运行时灵活加载不同的实现
    类。
  • 可扩展性:提供了一种易于扩展的机制,允许后期添加或替换实现类,而不需要修改现有
    代码。

Java泛型的作用是什么?

Java 泛型的作用是通过在编译时检查类型安全,允许程序员编写更通用和灵活的代码,避免在运行时发生类型转换错误。

作用:

  • 类型安全:泛型允许在编译时进行类型检查,确保在使用集合或其他泛型类时,不会出现类型不匹配的问题,减少了运行时的ClassCastException 错误。
  • 代码重用:泛型使代码可以适用于多种不同的类型,减少代码重复,提升可读性和维护性。
  • 消除显式类型转换:泛型允许在编译时指定类型参数,从而消除了运行时需要显式类型转换的麻烦。

Java的类加载过程是怎样的?

类加载指的是把类加载到JVM中。把二进制流存储到内存中,之后经过一番解析、处理转化成可用的class类。

二进制流可以来源于class文件,或通过字节码工具生成的字节码或来自于网络。只要符合格式的二进制流,JVM来者不拒。

类加载流程分为:

1.加载
2.连接
3.初始化

连接还能拆分为:验证、准备、解析三个阶段。

所以总共分为5个阶段:

  1. 加载:将二进制流读入内存中,生成一个Class对象。

  2. 验证:主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前JVM版本等等
    之类的验证。

  3. 准备:为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并
    且是初始值,比如int的初始值是0。

  4. 解析:将常量池的符号引用转化成直接引用。

符号引用可以理解为只是个替代的标签,比如你此时要做一个计划,暂时还没有人选,你设定了个A去做这个事。然后等计划真的要落地的时候肯定要找到确定的人选,到时候就是B去做一件事。
直接引用指的是一个真实引用,在内存中可以通过这个引用查找到目标。

  1. 初始化:这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。

Java中的BigDecimal

BigDecimal是Java中提供的一个用于高精度计算的类,属于java.math包。它提供对浮点数和定点数的精确控制,特别适用于金融和科学计算等需要高精度的领域。

主要特点:

  • 高精度:BigDecimal可以处理任意精度的数值,而不像float和double存在精度限制。
  • 不可变性:BigDecimal是不可变类,所有的算术运算都会返回新的BigDecimal对象,而不会修改原有对象(所以要注意性能问题)。
  • 丰富的功能:提供了加、减、乘、除、取余、舍入、比较等多种方法,并支持各种舍入模式。

使用new String(“fish”)语句会在Java中创建几个对象?

会创建1或2个字符串对象。

主要有两种情况:

  1. 如果字符串常量池中不存在字符串对象”fish”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
  2. 如果字符串常量池中已存在字符串对象”fish”的引用,则只会在堆中创建1个字符串对象”fish”。

Java中的迭代器(Iterator)

Iterator是Java集合框架中用于遍历集合元素的接口,允许开发者依次访问集合中的每一个元素,而不需要关心集合的具体实现。它提供了一种统一的方式来遍历List、Set等集合类型,通常与Collection类接口一起使用。

主要作用:

  • 迭代器使得遍历不同类型的集合更加简洁、统一,避免直接操作索引,提升了代码的可读性和可维护性。
  • 它支持在遍历过程中动态修改集合内容(例如删除元素,这在for-each循环中是会报错的)。
1
2
3
4
5
6
7
List<String> list = Arrays.asList("A""B""C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
}

因为 Iterator在遍历集合的过程中,如果检测到集合的结构发生了非迭代器自身的修改(比如使用List#remove()、List#add()直接修改集合),会抛出
ConcurrentModificationException。
这种机制称为“fail-fast”。

为了避免这种情况发生,修改集合时应使用Iterator的remove()方法,而非直接操作集合。

1
2
3
4
5
6
7
8
9
List<String> list = new ArrayList<>(Arrays.asList("A""B""C"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("B".equals(item)) {
iterator.remove(); //避免 fail-fast
}
}

Iterator缺点

  • 只能单向遍历,不能向前遍历。
  • 使用remove()只能删除最近一次通过next()方法获取的元素,删除的灵活性有限。

ListIterator:

ListIterator是Iterator的子接口,专门用于操作List集合。它可以双向遍历和元素修改。

  • hasPrevious():判断是否有上一个元素。
  • previous():返回上一个元素。
  • set(E e):将当前元素替换为指定的元素。
  • add(E e):在当前迭代位置之前插入一个新元素。

Java中的继承机制

Java 中的继承机制是面向对象编程的核心特性之一,允许一个类(子类)继承另一个类(父类)的属性和方法。继承机制使得类之间可以形成层次结构,支持代码重用和扩展。它是实现多态、抽象和代码复用的关键机制。

子类继承父类的字段和方法,可以重用和扩展父类的功能。Java使用extends关键字来表示类的继承关系。

Java 支持单继承,即一个类只能直接继承一个父类。子类可以继承父类的所有公共和受保护的成员,但不能继承父类的私有成员。

子类构造方法首先调用父类的无参构造方法,如果父类没有无参构造方法,子类必须显式调用父类的其他构造方法。

super关键字可以调用父类的方法或构造方法。

super关键字

继承的优缺点

优点:

  • 代码复用:子类可以复用父类的代码,减少重复实现。
  • 易于维护:可以通过修改父类代码来影响所有子类。

缺点:

  • 紧耦合:子类依赖于父类的实现,父类的修改可能会影响子类。
  • 灵活性差:继承层次结构可能会变得复杂,不易于调整或扩展。

java中的网络编程

Java 的网络编程主要利用java.net包,它提供了用于网络通信的基本类和接口。

Java 网络编程的基本概念:

  • IP地址:用于标识网络中的计算机。
  • 端口号:用于标识计算机上的具体应用程序或进程。
  • Socket(套接字):网络通信的基本单位,通过IP地址和端口号标识。
  • 协议:网络通信的规则,如TCP(传输控制协议)和UDP(用户数据报协议)。

Java 网络编程的核心类:

  • Socket:用于创建客户端套接字。
  • ServerSocket:用于创建服务器套接字。
  • DatagramSocket:用于创建支持UDP协议的套接字。
  • URL:用于处理统一资源定位符。
  • URLConnection:用于读取和写入URL引用的资源。

下面代码基于TCP通信:

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.io.*;
import java.net.*;

public class TCPServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Server is listening on port 8080");

while (true) {
Socket socket = serverSocket.accept();
//异步处理,优化可以用线程池
new ServerThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

class ServerThread extends Thread {
private Socket socket;

public ServerThread(Socket socket) {
this.socket = socket;
}

public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

// 读取客户端消息
String message = in.readLine();
System.out.println("Received: " + message);

// 响应客户端
out.println("Hello, client!");
} catch (IOException e) {
e.printStackTrace();
}
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;
import java.net.*;

public class TCPClient {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 8080);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

// 发送消息给服务器
out.println("Hello, server!");

// 接收服务器的响应
String response = in.readLine();
System.out.println("Server response: " + response);
} catch (IOException e) {
e.printStackTrace();
}
}
}

Java中wait()和sleep()的区别

wait()和Sleep()都是用于暂停线程的操作,但它们有明显的区别。

  1. 使用要求不同:
    • wait()方法必须在同步块或同步方法内调用,否则会抛出IllegalMonitirStateException。这是因为wait()依赖于对象锁来管理线程的等待和唤醒机制。调用后,当前线程会释放它持有的对象锁,并进入等待状态。
    • sleep()方法可以在任何上下文中调用,不需要获取对象锁。调用后,线程会进入休眠状态,但不会释放它持有的任何锁。
  2. 方法所属类不同:
    • wait()属于object类。
    • sleep()属于Thread类。
  3. 恢复方式不同:
    • wait()需要被其他线程通过notify()或notifyAll()显式唤醒,或被wait(long timeout)的超时参数唤醒。
    • sleep():在指定时间后自动恢复运行,或通过抛出InterruptedException恢复。
  4. 用途不同:
    • wait():通常用于线程间通信,配和notify()或notifyAll()来实现线程的协调工作。
    • sleep():用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延时。