Java基础
Java中的Exception和Error
Exception和Error都是Throwable类的子类(Java中只有继承了Throwable类的实例才能被throw和catch)。
总结来看:
Exception表示可以被处理的程序异常,Error表示系统及的不可恢复错误。
详细说明:
- Exception:是程序中可以处理的异常情况,表示程序逻辑或外部环境中的问题,可以通过代码进行恢复或处理。
如IOException
,SQLException
,NullPointerException
等
Exception又分为Checked Exception(编译期异常)和Unchecked Exception(运行时异常)
- Checked Exception:在编译时必须显式处理(如使用 try-catch
抛出)。如 IOException。 - Unchecked Exception:运行时异常,不需要显式捕获。常见的如
IllegalArgumentException 等,继承自
- Error:表示严重的错误,通常是JVM层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。例如内存耗尽(OutOfMemoryError)、栈溢出
(StackOverflowError) o
Error不应该被程序捕获或处理,因为一般出现这种错误时程序无法继续运行。
Java的多态
概念
多态是指同一个接口或父类引用变量可以指向不同的对象实例,并根据实际指向的对象类型执行相应的方法。
它允许同一方法在不同对象上表现出不同的行为,是面向对象编程(OOP)的核心特性之一。
多态的优点:
- 通过多态,程序可以灵活地处理不同类型的对象,降低代码耦合度,增强系统的可扩展性。新增子类或实现类时,无需修改原有代码,只需通过接口或父类引用调用即可。
多态其实是一种抽象行为。
举个例子:
一个人可以说学生,也可以是老师。
我们定义Persion类,用student类和teacher类分别继承它。
Persion类含一个方法work()。
那么这时,student类内重写work()为上课,teacher类内重写work()为教书。
使用时,对象都是persion,但new不同实现类,表现形式不同,这就是多态。
编译时多态和运行时多态
编译时多态和运行时多态是面向对象编程中多态性的两种实现方式,它们分别在不同的阶段决定方法的绑定。
- 编译时多态:通过方法重载实现,在编译时确定方法的调用。
- 运行时多态:通过方法重写实现,在运行时确定方法的调用。
编译时多态,也称为静态多态,是在编译阶段确定方法的调用。编译时多态主要通过方法重载(Method Overloading)实现。
方法重载;指在同一个类中定义多个方法,这些方法的名称相同但参数列表(参数的类型或数量)不同。Java编译器在编译时会根据方法调用时传入的参数类型和数量,决定调用哪一个重载方法。
运行时多态,也称为动态多态,是在运行时确定方法的调用。运行时多态通过方法重写(Method Overriding) 实现。
方法重写:子类重写父类的一个或多个方法。通过父类引用调用方法时,实际执行的是子类重写后的方法。这种多态性是在运行时根据对象的实际类型决定的。
Java中的参数传递是按值还是按引用?
在Java中,参数传递只有按值传递,无论是基本类型还是引用类型。
- 基本类型:如int、float等,存储在栈内存中,方法中对基本参数的操作只会影响传递的副本,原始变量的值不受影响。
- 引用类型:包括所有的对象和数组,引用类型的变量存储是对象在堆内存中的地址。传递时是地址的副本。方法内修改可以影响到传入对象的内容,但不会影响对象本身的地址。
Java方法重载和方法重写的区别
区别 | 重载 | 重写 |
---|---|---|
发生场景 | 同一个类中 | 继承关系的子父类之间 |
参数列表 | 必须不同(参数数量、类型、顺序) | 必须相同 |
返回类型 | 可以不同 | 必须与父类方法的返回类型相同,或是父类返回类型的子类 |
访问修饰符 | 不受影响 | 相同或更广 |
静态方法和非静态方法 | 都可以 | 只能重写非静态方法,(非静态方法可以被隐藏) |
异常处理 | 可以不同 | 子类异常不能抛出比父类更多的异常 |
Java内部类
Java 内部类是指在一个类的内部定义的类,Java支持多种类型的内部类,包括成员内部类、局部内部类、匿名内部类和静态内部类。内部类可以访问外部类的成员变量和方法,甚至包括私有的成员。
内部类的作用:
- 封装性:将逻辑相关的类封装在一起,提高类的内聚性。
- 访问外部类成员:内部类可以方便地访问外部类的成员变量和方法,尤其在需要操作外部类对象的场景下非常有用。
- 简化代码:对于只在一个地方使用的小类,内部类能减少冗余代码,简化结构。
- 事件处理:匿名内部类广泛用于实现回调函数或事件监听,简化了代码结构,特别是对于实现接口或抽象类的场景。
内部类类型:
- 成员内部类:非静态类,作为外部类的一个成员。它可以直接访问外部类的所有成员,包括私有成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14public 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
14public 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
14public 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
15public class OuterClass {
interface Greeting {
void greet();
}
public void sayHello() {
Greeting greeting = new Greeting() {
public void greet() {
System.out.println("Hello, World!");
}
};
greeting.greet();
}
}
Java中String、StringBuffer、StringBuilder的区别
- String
- 不可变:String是不可变类,每次修改都会创建新的String对象。
- 适合场景:用于字符串不会频繁变化的场景,例如少量的字符串拼接操作或字符串常量。
- StringBuffer
- 可变:String是可变的,可以进行字符串的追加、删除、插入等。
- 线程安全:内部使用synchornized关键字来保证多线程下的安全性。
- 适合场景:用于多线程环境中需要频繁修改字符串的场景。
- StringBuilder
- 可变:提供了与StringBuffer类似的操作接口。
- 非线程安全:不保证线程安全,性能比StringBuffer更高。
- 适合场景:适用于单线程环境下需要大量修改字符串的场景,如高频拼接操作。
总结
- String:不可变,适用少量字符串操作。
- StringBuffer:可变且线程安全,适合多线程环境中的频繁字符串修改。
- StringBuilder:可变且非线程安全,适合单线程环境中的高性能字符串处理。
什么是Java中的动态代理?
Java中的动态代理是一种在运行时创建代理对象的机制。动态代理允许程序在运行时决定代理对象的行为,而不需要在编译时确定。它通过代理模式为对象提供一种机制,使得可以在不修改目标对象的情况下对其进行增强或调整。
代理可以看作是调用目标的一个包装,通常用来调用真实的目标之前进行一些逻辑处理,消除一些重复代码。
静态代理指的是我们预先编码好一个代理类,而动态代理指的是运行时生成代理类。
动态代理主要用途
- 简化代码:通过代理模式,可以减少重复代码,尤其是在横切关注点(如日志记录、事务管理、权限控制等)方面。
- 增强灵活性:动态代理使得代码更具灵活性和可扩展性,因为代理对象是在运行时生成的,可以动态地改变行为。
- 实现AOP:动态代理是实现面向切面编程(AOP,Aspect-Oriented Programming)的基础,可以在方法调用前后插入额外的逻辑。
Java 动态代理与CGLIB代理:
- Java 动态代理:只能对接口进行代理,不支持对类进行代理。
- CGLIB代理:通过字节码技术动态生成目标类的子类来实现代理,支持对类(非接口)进行代理。
JDK代码示例:
1 | import java.lang.reflect.InvocationHandler; |
CGLIB代码实例:
1 | import net.sf.cglib.proxy.Enhancer; |
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个阶段:
加载:将二进制流读入内存中,生成一个Class对象。
验证:主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前JVM版本等等
之类的验证。准备:为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并
且是初始值,比如int的初始值是0。解析:将常量池的符号引用转化成直接引用。
符号引用可以理解为只是个替代的标签,比如你此时要做一个计划,暂时还没有人选,你设定了个A去做这个事。然后等计划真的要落地的时候肯定要找到确定的人选,到时候就是B去做一件事。
直接引用指的是一个真实引用,在内存中可以通过这个引用查找到目标。
- 初始化:这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。
Java中的BigDecimal
BigDecimal是Java中提供的一个用于高精度计算的类,属于java.math包。它提供对浮点数和定点数的精确控制,特别适用于金融和科学计算等需要高精度的领域。
主要特点:
- 高精度:BigDecimal可以处理任意精度的数值,而不像float和double存在精度限制。
- 不可变性:BigDecimal是不可变类,所有的算术运算都会返回新的BigDecimal对象,而不会修改原有对象(所以要注意性能问题)。
- 丰富的功能:提供了加、减、乘、除、取余、舍入、比较等多种方法,并支持各种舍入模式。
使用new String(“fish”)语句会在Java中创建几个对象?
会创建1或2个字符串对象。
主要有两种情况:
- 如果字符串常量池中不存在字符串对象”fish”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
- 如果字符串常量池中已存在字符串对象”fish”的引用,则只会在堆中创建1个字符串对象”fish”。
Java中的迭代器(Iterator)
Iterator是Java集合框架中用于遍历集合元素的接口,允许开发者依次访问集合中的每一个元素,而不需要关心集合的具体实现。它提供了一种统一的方式来遍历List、Set等集合类型,通常与Collection类接口一起使用。
主要作用:
- 迭代器使得遍历不同类型的集合更加简洁、统一,避免直接操作索引,提升了代码的可读性和可维护性。
- 它支持在遍历过程中动态修改集合内容(例如删除元素,这在for-each循环中是会报错的)。
1 | List<String> list = Arrays.asList("A", "B", "C"); |
因为 Iterator在遍历集合的过程中,如果检测到集合的结构发生了非迭代器自身的修改(比如使用List#remove()、List#add()直接修改集合),会抛出
ConcurrentModificationException。
这种机制称为“fail-fast”。
为了避免这种情况发生,修改集合时应使用Iterator的remove()方法,而非直接操作集合。
1 | List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); |
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 | import java.io.*; |
客户端:
1 | import java.io.*; |
Java中wait()和sleep()的区别
wait()和Sleep()都是用于暂停线程的操作,但它们有明显的区别。
- 使用要求不同:
- wait()方法必须在同步块或同步方法内调用,否则会抛出IllegalMonitirStateException。这是因为wait()依赖于对象锁来管理线程的等待和唤醒机制。调用后,当前线程会释放它持有的对象锁,并进入等待状态。
- sleep()方法可以在任何上下文中调用,不需要获取对象锁。调用后,线程会进入休眠状态,但不会释放它持有的任何锁。
- 方法所属类不同:
- wait()属于object类。
- sleep()属于Thread类。
- 恢复方式不同:
- wait()需要被其他线程通过notify()或notifyAll()显式唤醒,或被wait(long timeout)的超时参数唤醒。
- sleep():在指定时间后自动恢复运行,或通过抛出InterruptedException恢复。
- 用途不同:
- wait():通常用于线程间通信,配和notify()或notifyAll()来实现线程的协调工作。
- sleep():用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延时。