Java基础
约 6179 字大约 21 分钟
2025-01-15
数据类型
基本数据类型
Java 中有 8 种基本数据类型,分别为:
- 6 种数字类型:
- 4 种整数型:
byte、short、int、long - 2 种浮点型:
float、double
- 4 种整数型:
- 1 种字符类型:
char - 1 种布尔型:
boolean
这 8 种基本数据类型的默认值以及所占空间的大小如下:
| 基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
|---|---|---|---|---|
byte | 8 | 1 | 0 | -128 ~ 127 |
short | 16 | 2 | 0 | -32768(-2^15) ~ 32767(2^15 - 1) |
int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
long | 64 | 8 | 0L | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) |
float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
char | 16 | 2 | 'u0000' | 0 ~ 65535(2^16 - 1) |
boolean | 1 | false | true、false |
基本类型和包装类型
用途:
- 基本类型用于定义一些常量和局部变量;包装类型用于方法参数、对象属性
- 包装类型可用于泛型,而基本类型不可以。
存储方式:
- 基本数据类型的局部变量存放在 JVM 中的局部变量表中,基本数据类型的成员变量(未被
static修饰 )存放在 JVM 的堆中。 - 包装类型属于对象类型,存在于堆
- 基本数据类型的局部变量存放在 JVM 中的局部变量表中,基本数据类型的成员变量(未被
默认值:
- 成员变量包装类型不赋值就是
null - 基本类型有默认值且不是
null
- 成员变量包装类型不赋值就是
比较方式:
- 对于基本数据类型来说,
==比较的是值。 - 对于包装数据类型来说,
==比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用equals()方法。
- 对于基本数据类型来说,
注意注意:基本数据类型存放在栈中是一个常见的误区! 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中
public class Test {
// 成员变量,存放在堆中
int a = 10;
// 被 static 修饰,也存放在堆中,但属于类,不属于对象
// JDK1.7 静态变量从永久代移动了 Java 堆中
static int b = 20;
public void method() {
// 局部变量,存放在栈中
int c = 30;
static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量
}
}包装类型的缓存机制
Byte,Short,Integer,Long这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据Character创建了数值在 [0,127] 范围的缓存数据Boolean直接返回TrueorFalse。
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
- 举例一:
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false- 举例二:注意新建包装类型对象
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);
//Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象
//结果为fasle自动装箱/拆箱
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; //装箱
int n = i; //拆箱查看字节码文件,如下:
L1
LINENUMBER 8 L1
ALOAD 0
BIPUSH 10
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;
L2
LINENUMBER 9 L2
ALOAD 0
ALOAD 0
GETFIELD AutoBoxTest.i : Ljava/lang/Integer;
INVOKEVIRTUAL java/lang/Integer.intValue ()I
PUTFIELD AutoBoxTest.n : I
RETURN- 装箱其实就是调用了 包装类的
valueOf()方法 - 拆箱其实就是调用了
xxxValue()方法。
Integer i = 10 等价于 Integer i = Integer.valueOf(10)
int n = i 等价于 int n = i.intValue();
关键字
static 关键字
static 关键字用于声明静态成员(静态变量、静态方法)静态成员属于类,而不是类的实例。因此,无论创建多少个类的实例,静态成员只有一份。
使用场景:
- 类级别的共享数据
- 静态方法
- 单例模式的实例对象
JVM 层面:
- 静态变量、静态方法均存储于方法区中,而不是堆
- 对于静态变量,JVM 会在类加载时为其分配内存并初始化其默认值(如
null或0),确保被访问时,JVM 已经加载并完成任何静态初始化操作
final 关键字
final 关键字用于声明不可改变的字段、方法和类。
使用场景:
- 常量:当你希望某个变量的值不能改变时,可以使用
final。它使得变量成为常量,通常与static结合使用(static final)来表示类常量 - 防止方法被重写
- 防止类被继承
成员变量与局部变量
语法形式:
成员变量属于类,可以被
public,private,static等修饰符所修饰局部变量属于代码块或者方法,能被访问控制修饰符及
static所修饰;
存储方式:
成员变量(不带 static 修饰符)存在于堆内存
局部变量则存在于栈内存。
面向对象基础
面向对象三大特征
封装、继承、多态
封装:
- 封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息,但是可以提供一些可以被外界访问的方法来操作属性。
- 举例:我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。
继承:
- 是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法
多态:顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
区分接口和抽象类
共同点
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法(Java 8 可以用
default关键字在接口中定义默认方法)
不同点
单继承、多实现
成员变量修饰符不同、赋值不同:
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值
- 抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值
用途不同:
接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。
抽象类主要用于代码复用,强调的是所属关系。
深、浅、引用、对象拷贝
引用拷贝:创建一个指向对象的引用变量的拷贝,例子如下
Teacher teacher = new Teacher("riemann", 28); Teacher otherTeacher = teacher; sout(teacher==otherTeacher); // true对象拷贝:创建对象的一个拷贝,即创建新的对象,例子如下
Teacher teacher = new Teacher("riemann", 28); Teacher otherTeacher = (Teacher) teacher.clone(); Teacher teacher = new Teacher("riemann", 28); Teacher otherTeacher = teacher; sout(teacher==otherTeacher); // false浅拷贝:如果拷贝对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象
深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
以下图来描述浅拷贝、深拷贝、引用拷贝:

Object 类
==和 equals 的区别
==是运算符
- 若比较的对象是基本数据类型,则比较数值是否相等
- 若比较的对象是引用数据类型,则比较的是对象的内存地址是否相等
equals 是方法
equals 是比较基本数据类型,还是引用数据类型的变量,其比较的都是值,
只是引用类型变量存的值是对象的地址。引用类型对象变量其实是一个引用,它们的值是指向对象所在的内存地址。
equals()方法存在于 Object 类中,而 Object 类是所有类的父类。在 Object 类中定义了 equals 方法:
public boolean equals(Object obj) { return (this == obj); }
如果类未重写 equals 方法,调用 equals 时,会调用 Object 中的 equals 方法,实际使用的也是操作符==
如果类重写了 equals 方法,调用 equals 时,会调用该类自己的 equals 方法,一般是比较对象的内容是否相同。比如:
- String:比较字符串内容是否相同;
- Integer:比较对应的基本数据类型 int 的值是否相同。
String
String、StringBuffer、StringBuilder 区别
三者的区别可以从可变性、线程安全性、性能三个角度去分析:
可变性:
String是不可变的StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,不过没有使用final和private关键字修饰,最关键的是这个AbstractStringBuilder类还提供了很多修改字符串的方法比如append方法abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; } //... }
线程安全性:
String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能:
- 每次对
String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对 StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。- 相同情况下使用
StringBuilder相比使用StringBuffer仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
- 每次对
- 总结:
- 操作少量的数据: 适用
String - 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder - 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
- 操作少量的数据: 适用
String 为什么是不可变的
String类中使用 private 和 final 两个关键字修饰字符数组(作用:保存字符串)- 被
final关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象 - 但是当数组存放的对象是引用类型时,这个数组保存的字符串的内容是可变的(虽然不能再指向其他对象,但是可以修改里面对象的数据)
- 真正不可变的原因:
- 存储字符串的数组被
final修饰且为私有的,并且String类没有提供/暴露修改这个字符串的方法。 String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变。
- 存储字符串的数组被
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}内存分配不同
String a = "111";使用的是字符串常量池,引用池中已有的字符串对象,节省内存。String a = new String("111");会在堆内存中创建一个新的String对象,即使"111"字符串已经存在于常量池中。
Java 引用
在 Java 中,引用(Reference)是用来指向对象的实体,并提供对这些对象的访问。
从 JDK 1.2 版本开始,对象的引用被划分为 4 种级别,从而使程序能更加灵活地控制对象的生命周期,这 4 种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
| 引用类型 | 被回收时间点 | 用途 | 死亡时间 |
|---|---|---|---|
| 强引用 | 从来不会 | 对象的一般状态 | 将强引用弱化,并 gc 后 |
| 软引用 | 内存不足 | 对象缓存 | 内存不足,并 gc 后 |
| 弱引用 | 垃圾回收 | 对象缓存 | gc 后 |
| 虚引用 | 未知 | 未知 | 未知 |
# 强引用
Person p=new Person()
# 软引用
SoftReference<String> softRef=new SoftReference<String>(str);
# 弱引用
WeakReference<Object> w1 = new WeakReference<Object>(o1);异常
定义
错误(Error)与异常(Exception)的区别:
- Error:代表 JVM 自身的异常,无法通过程序来修正,最可靠的方式就是尽可能快地停止 JVM 的运行,常见的 Error 有:
java.lang.VirtualMachineError(Java 虚拟机运行错误):当 Java 虚拟机崩溃或用尽了它继续操作所需的资源时,抛出该错误java.lang.StackOverflowError(栈溢出错误):当应用程序递归太深而发生堆栈溢出时,抛出该错误java.lang.OutOfMemoryError(内存溢出):内存溢出或没有可用的内存提供给垃圾回收器时,产生这个错误
- Exception:代表程序运行中发生了意料之外的事情,这些意外的事情可以被 Java 异常处理机制处理。
Exception,指程序本身可以处理的错误(可以向上抛出或者捕获处理)
异常类继承层次
Thowable
异常是对象,异常封装成类 Exception,所有的异常都直接或间接继承自 Throwable 类。
Throwable 类有两个直接的子类,Error 和 Exception。
- 可以通过继承 Exception 或 Exception 的子类来创建自己的异常类。
- Exception 是程序可以恢复的异常。如除零异常、空指针访问、网络连接中断、读取不存在的文件等。
- Exception 类分为受检查异常和运行时异常
- 受检查异常(必检异常):是除 RuntimeException 以外的异常,特点是编译器会强制程序员检查并通过 try-catch 块处理它们,或在方法头进行声明。如处理数据库异常的 SQLException,处理读写异常的 IOException
- 运行时异常(免检异常):是继承 RuntimeExceptionl 类的直接或间接类。编译器不检查这类异常是否进行了处理,也就是对于这类异常不捕获也不抛出,程序也可以编译通过。一旦运行时异常时就会导致程序的终止。如访问一个数组的越界元素,会抛出一个 IndexOutofBoundsException 异常。

常见异常
- 检查性异常:
- IOException:IO 异常
- FileNotFoundException:文件找不到异常
- ClassNotFoundExcetption:类找不到异常
- 运行时异常:
- NullPointerException:空指针异常
- IndexOutOfBoundsException:数组下标越界异常
- ClassCastException:类型转换异常
异常的处理模型
Java 的异常处理模型基于三种操作:
- 声明一个异常
- 抛出一个异常或者捕获一个异常

声明并抛出异常
throws 通常在方法首部的声明后抛出异常,抛出的是可能发生的异常。当该方法被调用的时候必须捕获,或者也可以再次抛出异常,最终由 Java 虚拟机处理。
用来声明一个方法可能产生的所有异常(用,分隔), 不做任何处理而是将异常往上传,谁调用我我就抛给谁。
举例:
class MyAnimation{
public Image loadImage(String s) throws IOException{
...
}
}throw 抛出异常
throw 关键字通常用在方法体中,并且抛出一个异常对象。执行 throw 则一定抛出了某种异常,只能抛出一个异常对象
举例:
String readData(Scanner in)throws EOFException{
while(...){
if(!in.hasNext())//遇到EOFException异常
if(n<len){
throw new EOFException();
}
}
}自定义异常
自定义异常类
class CustomException extends Exception {
private String customMessage;
public CustomException(String message) {
super(message);
this.customMessage = message;
}
public String getCustomMessage() {
return customMessage;
}
}测试类
class Example {
public static void main(String[] args) {
try {
// 模拟条件触发自定义异常抛出
int age = -1;
if (age < 0) {
throw new CustomException("年龄不能为负数");
}
} catch (CustomException e) {
System.out.println("捕获到自定义异常:" + e.getCustomMessage());
}
}
}反射
反射定义
Java 反射机制指的是在 Java 程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法,并都能够调用它的任意一个属性和方法。
在 Java 编程语言中,反射是一种强有力的工具,是面向抽象编程的一种实现方式,它能使代码语句更加灵活,极大提高代码的运行时装配能力。Java 反射机制允许编程人员在对类未知的情况下,获取类相关信息的方式变得更加多样灵活,调用类中相应方法,是 Java增加其灵活性与动态性的一种机制。
总结一下,Java 反射机制有如下作用:
- 反射机制极大的提高了程序的灵活性和扩展性,降低模块的耦合性,提高自身的适应能力;
- 通过反射机制可以让程序创建和控制任何类的对象,无需提前硬编码目标类;
- 使用反射机制能够在运行时构造一个类的对象、判断一个类所具有的成员变量和方法、调用一个对象的方法;
- 反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中。
反射原理
- 首先我们需要了解 Java 程序运行的过程,该过程包含两个阶段:编译和运行。
- 在程序编译阶段,Java 代码会通过 JDK 编译成 .class 字节码文件;
- 在程序运行阶段,JVM会去调用业务逻辑对应需要的的字节码文件,生成对应的Class 对象,并调用其中的属性方法完成业务逻辑。
- Java 的反射机制原理:在程序运行阶段,主动让 JVM 去加载某个 .class 文件生成 Class 对象,并调用其中的方法和属性。
使用反射
四个常用的类
| 类名 | 作用 |
|---|---|
| Class 类 | 类的实体,在运行的 Java 应用程序中表示类和接口 |
| Field 类 | 类的成员变量(成员变量也称为类的属性) |
| Method 类 | 类的方法 |
| Constructor 类 | 类的构造方法 |
获取类对象
- 具体类的类名的 class 属性
Class bookClass = Book.class;
//输出类名
System.out.println(bookClass.getName());- 对象实例的 getClass 方法
Book book = new Book();
Class bookClass = book.getClass();
//输出类名
System.out.println(bookClass.getName());- class.forName(类的全路径)
Class bookClass;
try {
bookClass = Class.forName("test.Book");
//输出类名
System.out.println(bookClass.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}- 类加载器的 loadClass(类的全路径)
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");获取类对象的构造函数
- 当获取到一个类的 Class 对象之后,可以调用 Class 对象的 getDeclaredConstructors()方法获取该类的构造函数,如下:
// 反射所有声明的构造方法
public static void reflectAllConstructor() {
System.out.println(TAG + "=============获取所有的声明的构造函数==============");
try {
Class<?> classBook = Class.forName("test.Book");
Constructor<?>[] constructorsBook = classBook
.getDeclaredConstructors();
for (Constructor constructor : constructorsBook) {
System.out.println(TAG + constructor);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}- 获取了构造函数之后,调用 Constructor 类对象的 newInstance()即可构造出我们想要类的对象,如下:
Book book = (Book)constructor.newInstance();获取类对象的方法
- 当我们得到了一个 Class 对象之后,我们可以获取该类的所有方法,如下:
- getDeclaredMethods()和 getMethods()都可以获取到类的方法,辨别?
- getMethods()获取了自己定义的公用方法(private 获取不了),还把 Object 父类的公用方法也获取了
- getDeclaredMethods()只能获取自己类中定义的方法,但是可以获取到 private 方法
// 反射所有的public的函数
public static void reflectPublicMethods() {
System.out.println(TAG + "=============获取所有的public的函数==============");
try {
Class<?> classBook = Class.forName("test.Book");
Method[] methodsBook = classBook.getMethods();
for (Method method : methodsBook) {
System.out.println(TAG + method);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 反射所有的声明的方法
public static void reflectAllMethods() {
System.out.println(TAG + "=============获取所有的声明的函数==============");
try {
Class<?> classBook = Class.forName("test.Book");
Method[] methodsBook = classBook.getDeclaredMethods();
for (Method method : methodsBook) {
System.out.println(TAG + method);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}获取类对象的属性
当我们得到了一个 Class 对象之后,我们可以获取该类的所有属性,代码如下:
同 Methods,获取属性也有 getDeclaredFields()和 getFields()两种。
// 反射所有的public的属性
public static void reflectPublicFields() {
System.out.println(TAG + "=============获取所有的public的属性==============");
try {
Class<?> classBook = Class.forName("test.Book");
Field[] fieldsBook = classBook.getFields();
for (Field field : fieldsBook) {
System.out.println(TAG + field);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 反射所有的声明的属性
public static void reflectAllFields() {
System.out.println(TAG + "=============获取所有的声明的属性==============");
try {
Class<?> classBook = Class.forName("test.Book");
Field[] fieldsBook = classBook.getDeclaredFields();
for (Field field : fieldsBook) {
System.out.println(TAG + field);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}反射优缺点
- 优点:让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- 缺点:
- 同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。
- 另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
动态代理和静态代理区别
- 静态代理是指在编译时就已经确定了代理对象和目标对象的关系,代理类和目标类都需要实现相同的接口,代理类持有目标对象,并在方法调用前后进行额外的操作
- 动态代理是指在运行时生成代理对象,而无需手动编写代理类。Java 的动态代理机制是基于反射实现的,通过使用 Proxy 类和 InvocationHandler 接口来实现动态代理
- 定义一个接口,作为目标接口
- 创建一个实现 InvocationHandler 接口的类,重写 invoke 方法,其中使用 method.invoke 执行目标方法,并实现方法增强
- 测试类:使用 Proxy 类的静态方法 newProxyInstance()生成代理对象,同时指定目标对象和 InvocationHandler,最后使用代理对象执行目标方法
- 区别:
- 灵活性:动态代理更加灵活,不需要针对每个目标类都创建一个代理类
- JVM:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件,动态代理是在运行时动态生成类字节码
JDK 动态代理和 CGLIB 动态代理的区别
- JDK 动态代理只能只能代理实现了接口的类或者直接代理接口
- CGLIB 可以代理接口还可以代理未实现任何接口的类
- CGLIB 动态代理是通过继承目标类并重写其方法来创建代理对象,因此不能代理声明为 final 类型的类和方法
- 效率: JDK 动态代理更优秀
Java 流
流类型
在编码方式角度看,可分为字节流和字符流
在字节流中,Input 用于读取文件,Output 用于写入文件;在字符流中,Reader 用于读取文件,Writer 用于写入文件
- 字节流
- InputStream
- FileInputStream
- ObjectInputStream
- ByteArrayInputStream
- OutputStream
- FileOutputSteam
- ObjectOutputStream
- ByteArrayOutputStream
- InputStream
- 字符流
- Reader
- BufferedRead
- InputStreamReader
- StringReader
- Writer
- BufferedWriter
- OutputStreamWriter
- StringWriter
- Reader
为啥叫 IO 流
流
在程序中,所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。
字节流
Java 中的字节流处理的最基本单位为单个字节,它通常用来处理二进制数据。Java 中最基本的两个字节流类是 InputStream 和 OutputStream,分别代表了最基本的输入字节流和输出字节流。
字符流
Java 中的字符流处理的最基本的单元是 Unicode 码元(大小 2 字节),它通常用来处理文本数据,例如字符、字符数组或字符串。Java 中的 String 类型默认就把字符以 Unicode 规则编码而后存储在内存中。
版权所有
版权归属:haipeng-lin