Java基础
Java基础
JVM、JRE、JDK有什么区别
JVM是java虚拟机,负责将字节码解释或编译为本地机器代码,并在运行时提供必要的支持,比如内存管理、垃圾回收等等
JRE是java运行时环境,包括了jvm、核心类库和其他支持运行java程序的文件。
JDK是用于开发Java程序的完整运行环境,包含了JRE,以及用于开发、调试和监控java应用程序的工具。
Java从编译到执行,经过了哪些步骤?
- 我认为就4个步骤:编译->加载->解释->执行。编译:将源码文件编译成JVM可以解释的class文件。编译过程会对源代码程序做 「语法分析」「语义分析」「注解处理」等等处理,最后才生成字节码文件。比如对泛型的擦除干的。
- 加载:class文件加载到JVM中,加载又可以分为加载、连接、初始化
- 解释:JVM通过解释或者编译将字节码转换为机器码
- 执行:最后操作系统把解释器解析出来的机器代码,调用系统的硬件执行最终的程序指令
Java语言
JDK8有什么新特性?
- 引入了lambda表达式。是一种匿名函数,将代码像数据一样进行传递,可以写出更加简洁、灵活的代码
- 用元空间替换永久代。因为JRockit 没有永久代,而JRockit 要和 Hotspot 融合,所以把 Hotspot 永久代给去了。永久代满了也会触发 fuill gc,触发了回收但是永久代本身回收率又很低,所以很不划算。
- Java 8 引入了新的日期和时间 API(位于 java.time 包中),它们更加简洁和易于使用,解决了旧版日期时间 API 的许多问题。 例如 util包下的Date、Calendar都是可变类且线程不安全。而新的日期类都是不可变的,一旦创建就不能修改,这样可以避免意外的修改,提升代码的安全性和可维护性。 Date 本身不包含时区信息,必须使用 Calendar 类来处理时区,但使用起来非常复杂且容易出错。新 API提供了专门的时区类(如 ZonedDateTime,OffsetDateTime,ZoneId 等),简化了时区处理。
- 接口中允许有方法的默认实现
- Stream流,支持一种高效的方式来处理数据,支持链式操作
- CompletableFuture,提供了一种新的异步编程模型。
Java语言有什么优缺点
- 优点:
- 相对简单易学(语法简单,上手容易);
- 跨平台( java编译成的字节码可以在任何支持jvm的平台上运行,Java 虚拟机实现平台无关性);
- 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
- 可靠性(具备异常处理和自动内存管理机制);
- 生态【Java生态圈齐全,存在丰富第三方类库、企业级框架、各种中间件...】
- 缺点:
- 性能问题:Java的性能通常比C、C++等编译型语言略差,主要由于JVM的解释执行的开销。然而,通过JIT编译,Java性能已经有了很大提升。
- 内存消耗:Java程序通常比C、C++程序消耗更多的内存【JVM本身占用一定的内存、JVM进行垃圾回收需要占用一定内存、对象的内存开销。】
- 启动时间较长:因为JVM需要初始化,Java应用程序的启动时间通常比一些本地编译的程序要长
编译型语言和解释型语言
- 解释型语言
解释型语言的代码不需要编译成独立的可执行文件,而是直接在运行时被解释器逐行执行。常见的解释型语言有Python、JavaScript等。这些语言通常具有较好的跨平台特性,因为它们的源代码可以在不同操作上通过解释器运行。
- 编译型语言
源代码在程序执行前需要经过编译,在编译器的处理下被转换成机器代码或者中间代码,并生成独立的可执行文件。常见的编译型语言有C、C++、Go等
- 编译型语言的执行速度通常比解释型语言快,因为它在运行前已经将代码转换成机器代码,不需要再逐行解释。但是编译型语言需要专门的编译过程,可能会有额外的编译时间。
Java是一种特殊的编译型语言。在Java中,源代码首先被编译器(Java编译器,通常称为javac)编译成字节码(中间代码)。
然后,在Java虚拟机(JVM)上运行时,字节码会被解释器逐行解释执行。或者通过Just-In-Time(JIT)及时编译器动态地将热点码转直接转换成的机器码,提高代码的执行效率。
因此,可以说Java既具备解释型语言的特点(先将源代码编译成字节码,再通过解释器逐行解释字节码),又具备解释型语言的特点(JIT编译器可以直接将源代码编译成字节码) “解释和编译并存”
解释执行效率低,有什么优化方式吗
即时编译(JIT)技术
JIT编译:即时编译器Java程序运行时,发现热点代买,就会在运行的时候,将字节码编译成机器码,从而提高执行速度。例如,Python的PyPy解释器就采用了JIT技术。
预先编译(AOT)技术
编译静态代码:对于那些不经常变化的代码,在代码运行前就编译成机器码。好处是减少运行时编译的开销,应用程序启动时可以直接加载和执行预编译的代码,提高启动速度。
JIT和AOT的区别、优缺点
优劣
JIT优点:
- 可以根据当前硬件情况、程序的运行情况实时编译生成最优的机器指令序列
- 可以根据进程中内存的实际情况调整代码,使内存能够更充分的利用
JIT缺点:
- 编译需要占用运行时资源,可能会导致进程卡顿
- 启动时间延迟:JIT编译是在程序运行时进行的,因此初次运行时会有编译开销;在程序刚启动时,很多代码尚未被JIT编译,可能导致初次运行性能较差。
AOT优点:
- 在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗
AOT缺点:
- AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理
- 将提前编译的内容保存会占用更多的外存
Java和C++的区别
Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 有垃圾回收机制(GC),不需要程序员手动释放无用内存。
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以实现。
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
标识符和关键字
标识符就是类、变量、方法的名字。
Java中,有一些标识符已经被赋予了明确的含义,这种就是关键字
访问控制修饰符
private、protected、public
注意:Java的访问控制是停留在编译层的,也就是它不会在.class文件中留下任何的痕迹,只在编译的时候进行访问控制的检查。其实,通过反射的手段,是可以访问任何包下任何类中的成员,访问类的私有成员也是可能的。
区别:
- public:可以被所有其他任何类所访问。
- protected:同一个包中类和所有子类可以访问。
- default(默认):同一包中的类可以访问,声明时没有加修饰符。
- private:只能被自己访问和修改。
注意:在继承的时候,他们的区别。
- public 声明的在子类变为public
- protected 声明的子类变为private
- private 声明的在子类中不可用
break、continue、return的区别
- continue:指跳出当前的这一次循环,继续下一次循环。
- break:指跳出整个循环体,继续执行循环下面的语句。
- return 结束该方法的运行。return;用于没有返回值函数的方法;return value;用于有返回值函数的方法。
面向对象的三个特性
- 封装
将数据和方法封装在一个类中,可以通过访问控制修饰符(如private、protected、public)来限制对类的成员的访问。封装能够隐藏对象的内部实现细节,只暴露必要的方法。
- 继承
子类继承父类,可以使用父类的一些方法和变量,并父类的基础上进行复用、扩展和修改。
- 多态
子类重写父类的方法,使得父类的同一个行为在子类中可以有不同实现。通过多态,代码更加通用、灵活。使用父类的引用指向子类的实例。
面向对象和面向过程的区别
- 面向对象会把数据和方法抽象出对象,作为程序的基本单元,注重代码的复用和灵活性。
- 面向过程更关注步骤和流程,通过一系列过程来实现数据的转换。
方法重载和重写的区别?
重载:在同一个类(或者父类和子类之间)中定义多个方法,它们具有相同的名字但参数列表不同,返回值和访问修饰符也可以不同。主要用于提供相同功能、不同参数的实现。
重写:前提是子类继承父类,并在子类中定义一个父类具有的相同方法,子类进行特定实现。主要用于实现运行时多态性。
什么是内部类,有什么作用?
内部类顾名思义就是一个类内部的类,主要作用是封装、逻辑分组、使代码结构更加清晰。
通过内部类,可以将逻辑上相关的类组织在一起,提升封装性和代码的可读性。
- 成员内部类,在成员变量的位置定义
- 静态内部类,static修饰,其实它就相当于一个外部类,可以独立于外部类使用,所以更多的只是表明类结构
- 匿名内部类,没有类名的内部类,用于简化实现接口和创建类的代码。
怎么新建一个对象实例
- new关键字
- 类.class.newInstance()
Employee emp3 = Employee.class.newInstance();
- 构造器.newInstance()
Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
对象实例和对象引用有什么不同
new 创建一个对象实例,对象实例放在堆中,对象引用放在栈中,对象引用指向对象实例。
- 一个对象可以引用可以指向0或1个对象实例
Person p; //p引用没有指向对象
p=new Person(); //对象引用指向对象实例
- 一个对象实例可以有多个对象引用指向它【这里a=p,引用拷贝只复制了对象引用,没有复制对象实例,因此有两个对象引用指向堆中的对象实例。并且使用==比较的是他们指向的内存地址是否相等】
Person p=new Person();
Person a=p;
System.out.println(a==p);
Java是引用传递还是值传递
在Java中,无论是基本数据类型还是引用数据类型,都是值传递。引用类型传递的是对象在堆中的地址。
如果一个类没有声明构造方法,能正确运行吗?
构造方法主要完成对象的初始化工作,一个类会有默认的无参构造方法,但是如果我们自己添加了构造方法的话,java就不会添加无参构造方法了。【重载有参构造方法时,记得补上无参构造方法】
- 构造方法特点
- 名字与类名相同
- 没有返回值,但是不能用void声明
- 生成类的对象实例时自动执行,无需调用。
- 不能被重写(override),但是可以被重载(overload)
接口和抽象类的共同点和区别
共同点:
- 都不能被实例化。
- 都可以包含抽象方法,接口只能定义抽象方法,抽象类可以定义普通方法。
- 在JDK8版本中,都可以有默认实现的方法【JDK8之前,接口只能有抽象方法】
// 抽象方法
void abstractMethod();
// 默认方法
default void defaultMethod() {
System.out.println("This is a default method.");
}
- 实现接口和实现抽象类的子类需要实现接口或抽象类的所有方法,如果没有实现所有方法,还要加abstract关键词修饰,变成抽象类。
区别:
- 接口用于对类的行为进行约束,是自顶向下的,也就是先约定接口,再实现。抽象类主要用于代码复用,强调的是所属关系,是自底向上的,也就是先写,然后发现代码能复用,抽取出一个抽象类。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值。【首先,接口提供一种“统一”的协议,属性也要满足这个协议,如果有变量的话,与接口提供的统一抽象这个思想是抵触的,所以接口中的属性必然是常量,只能读不能改,这样才能为实现接口的对象提供一个统一的属性】而抽象类的成员变量可以用各种各样的修饰符修饰。
abstract和final能否共用?
答:抽象类需要被继承才能使用,而被final修饰的类无法被继承,所以abstract和final是不能共存的。
Java为什么不支持多重继承
会存在菱形继承导致的语意不明,应当通过接口来进行多实现。BC继承了A,然后D继承了BC,假设此时A要调用D中的方法,如果B和C对该方法有不同的实现,此时就会出现歧义。
什么是序列化和反序列化
序列化就是将对象转换成字节序列格式,便于存储和传输;反序列化就是将字节序列转换成对象的过程。
什么是Java中的不可变类,怎么实现?
不可变类是指类初始化后,就不能修改对象的值。String就是一个常见的不可变类。
实现:类用final修饰,不能继承。变量使用private和final修饰,并且不提供set方法。如果需有修改的需求,也是返回一个新的对象作为结果。
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
- 深拷贝:完全拷贝整个对象,包括基本类型和引用类型,堆中的引用对象也会复制一份。
- 浅拷贝:拷贝对象中的基本类型和引用,堆中的引用对象和拷贝对象共享。
- 引用拷贝就是,拷贝一个引用,不会在堆上创建一个新对象,两个不同的引用指向同一个对象
Object类的常见方法
- getClass():返回当前运行时对象的 Class 对象
- hashCode():返回对象的32位哈希码【hashcode()方法使用的哈希算法可能会发生哈希碰撞。因此,hashcode值相等,对象不一定相等】
- equals():比较 2 个对象的内存地址是否相等【具体的比如像String类对这个方法进行重写,比较值】
- clone():返回当前对象的一份拷贝【浅拷贝】
- toString():返回类的名字实例的哈希码的 16 进制的字符串。
- notify()、notiffyAll():唤醒线程
- wait():wait方法释放锁,sleep方法没有释放锁
- finalize():实例被垃圾回收器回收的时候触发的操作
hashcode()和equals()方法是什么?
hashcode()是一个native方法,本质就是返回一个哈希码,int值,表示对象的内存地址
为什么重写equals()方法必须重写hashcode()方法
- 因为进行比较的话,会先比较hashcode,如果hashcode不相等,那就直接认为这两个对象不相等了。
进行比较的时候,会先判断两个对象的 hashCode 是否相同,比较的是两个对象的内存地址,结果是 false,那么 equals 方法就不会执行了,直接返回false,这和我们预期的结果不一致。
但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的 hashCode 方法,
@Override
public int hashCode() {
// 对比 name 和 age 是否相等
return Objects.hash(name, age);
}
对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了。
数据类型
引用数据类型
类、接口、数组、枚举、注解
基本数据类型
Java中有8中基本数据类型
- 6 种数字类型:
- 4 种整数型:byte、short、int、long
- 2 种浮点型:float、double
- 1 种字符类型:char
- 1 种布尔型:boolean
基本数据类型和包装数据类型类型的区别
因为Java是种面向对象的,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将int、double等类型放进去的。因为集合的容器要求元素是Object类型。
为了让基本类型也具有对象的特征,就出现了包装类型,使得基本数据类型有了对象的性质,丰富了基本类型的操作。
- 初始值不同
初始值不同,基本类型的初始值如0或false,而包装类型的初始值为null。
- 存储位置的不同
基本数据类型的局部变量存在于Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象,保存在堆上。
- 占用空间
相比较于基本类型而言,包装类型需要占用更多的内存空间。
- 比较方式:对于基本数据类型来说,
==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址。包装类对象之间实例值的比较,使用equals()
方法。【比如Integer中重写equals方法,取出intValue()再比较】
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
自动装箱和自动拆箱
自动装箱指Java编译器自动把基本类型转换成对应的包装类型。自动拆箱就是Java编译器自动把包装类型转换成基本类型。
Integer chenmo = Integer.valueOf(10);
int wanger = chenmo.intValue();
注意的事项:
- 频繁的拆装箱操作会影响性能
- 在拆箱操作中,如果包装类为null,会抛出空指针异常
包装数据类型的缓存机制
实践发现大部分数据操作都集中在比较小的范围,Java包装类型的大部分都用到了缓存机制来提升性能。Float和Double是没有缓存机制的,毕竟是小数,能存的数太多了。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
判断值是否在这个范围内,在这个范围内就去缓存(IntegerCache)中取。
如果超出对应范围仍然会去创建新的对象。
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。
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) 会直接创建新的对象。
因此,答案是 false。
为什么浮点数运算的时候会有精度丢失的风险?
计算机是二进制的,而且在表示一个数字时,宽度是有限的,有些小数(0.2)存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
什么是Java的BigDecimal?
BigDecimal是Java中提供的一个用于高精度计算的类,属于java.math包。提供对浮点数和定点数的精确控制,适用于金融和科学计算等需要高精度的场景。
十进制整数在转化成二进制数时不会有精度问题,那么把十进制小数扩大N倍让它在整数的维度上进行计算,并保留相应的精度信息。
成员变量和局部变量的区别
- 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存。局部变量则存在于栈内存。
- 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
为什么成员变量有默认值?
- 如果没有默认值,变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。
- 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。
静态变量有什么作用?
静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享。静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
静态方法为什么不能调用非静态成员?
静态方法是属于类的,在类加载的时候就会分配方法区的内存;而非静态成员属于实例对象,只有在对象实例化后才存在。因此已经存在内存中的静态方法去调用还不在内存中的非静态成员,属于非法操作。
字符串String
字符串拼接用什么?
首先方式分两种,“+”或使用stringbuilder.append()方法,但是如果字符串对象通过“+”号进行字符串拼接,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,而是每加一次,就创建一个stringbuilder对象,会导致创建过多的 StringBuilder 对象。
字符串常量池了解吗?
字符串常量池耗针对字符串,专门在堆中开辟的一块区域,主要目的是为了避免字符串的重复创建。
主要体现在:
- String i=new String("abc"),创建了几个对象?如果字符串常量池中没有"abc"这个对象,那么会在堆和字符串常量池中分别创建一个,然后再把对中字符串的对象地址返回给变量s。如果字符串常量池中已经有了"abc"这个对象,那就只会在堆中创建一个,然后把堆中字符串的地址返回给变量s
- 为啥要在字符串常量池中再创建一个咧?【因为如果使用 String s="abc"的话,如果有,不创建任何对象,直接把字符串常量池中的对象内存地址返回给变量;如果没有,只在字符串常量池中创建字符串对象,再把地址返回给对象。】
- 使用new方法会在堆中(非字符串常量值区)创建一个对象,使用双引号会直接利用字符串常量池
字符串反转方法
- 因为string本身没有reverse()方法,所以可以将string转换成stringbuilder或者stringbuffer,再使用reverse方法
String s="abc";
StringBuilder sb=new StringBuilder(s);
System.out.println(sb.reverse());
StringBuffer sb2=new StringBuffer(s);
System.out.println(sb2.reverse());
- 双指针分别指向字符数组的头和尾,开始交换,交换完指针往中间收缩直到相遇退出
- 将string转换成char数组,倒序遍历放到临时数组中,再将临时char数组转换为字符串
- 通过charAt方法逆序接收字符
字符串拼接方法
可以采用"+"号,因为加号本身就是新建了一个stringbuilder,然后利用stringbuilder的append方法,但是如果多次使用"+"号的话,编译器不会复用stringbuilder对象,会创建多个stringbuilder对象。
所以还是,直接将string转换成stringbuilder或者stringbuffer,再使用append方法进行拼接
String、StringBuffer、StringBuilder 的区别?
可变性
String 是不可变的。
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了synchronized关键字,或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
对于三者使用的总结:
- 少量字符串操作或者需要字符串常量池进行优化的场景: 适用 String
- 单线程环境下频繁操作字符串: 适用 StringBuilder
- 多线程环境下频繁操作字符串: 适用 StringBuffer
String 为什么是不可变的?
- 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供修改这个字符串的方法。
- String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
String s1 = new String("abc");这句话创建了几个字符串对象?
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”,那么它会在堆上创建两个字符串对象,其中一个会被保存在字符串常量池中。
2、如果字符串常量池中已存在字符串对象“abc”则只会在堆中创建 1 个字符串对象abc。
StringBuilder的底层实现
stringbuilder用于动态修改字符串,底层使用char数组来存储字符、count记录存放的字符数。
无参构造,16;有参构造 参数+16;扩容 翻两倍+2.
为什么JDK9中将String的char数组改成byte数组?
主要是为了节省空间。
JDK9之前,String类基于char数组实现,每个字符占用两个字节。但是如果当前字符仅需要一个字节空间,比如Latin-1字符,就会造成空间的浪费。因此,JDK9使用byte数组,并使用coder变量来识别编码方式(UTF-16或者Latin-1)
Latin1是什么?
Latin1是国际标准编码,是单字节编码,在ASCII码的基础上,利用了它未利用的最高位,扩充了128个字符,因此Latin1可以表示256个字符,向下兼容ASCII。
常量优化
常量折叠指 编译期间会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string"; 。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
- final 修饰的基本数据类型和字符串变量字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
异常
Java中Exception和Error的区别
两个类都继承自Throwable类, Exception是可以预料的异常情况,应该被捕获并进行相应处理,Error指的是正常情况下不太可能出现的情况,会导致程序处于不正常的状态,所以不需要被捕获,因为捕获了也无济于事。
异常处理注意点
- 尽量不要捕获Exception这种通用异常,而是捕获特定的异常,使得代码更加清晰直观
- 捕获异常后,应当将详细信息记录到日志中,有利于高效的排查错误
- 不要延迟处理异常。如果调用了很多别的方法才爆出异常,可能会输出很多异常信息。
- try-catch的范围能小则小,因为try-catch中的代码会影响JVM对代码的优化
- 不要通过异常来控制程序流程,一些可以使用if-else来进行的判断,就不要用异常,异常相对而言是低效的
- 不要在finally块中处理返回值。会覆盖try语句块中的return,导致程序运行和预期不一致。
运行时异常和编译时异常的区别
编译时异常,checked exception是在编译阶段检查代码可能出现的异常,需要显示的捕获或者抛出,否则编译就会报错,常见的有IOException、FileNotFoundException等。
运行时异常指,unchecked exception在运行期间可能抛出的异常,编译时期不强制要求处理,因为可以通过完善代码来避免。
try-catch-finally 如何使用?
- try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- catch块:用于处理 try 捕获到的异常。
- finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
finally中的代码一定会执行嘛?
- finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
System.exit(1);
- 程序所在的线程关闭。
- 关闭 CPU。
哪个部分可以省略
catch和finally中有一个可以省略。
- 省略 catch 语句,在方法申明统一将异常抛出,finally 语句块把流对象关闭、销毁系统对象...
- 省略finally:仅仅使用 catch 语句块对异常进行处理。
静态方法和实例方法的区别
- 静态方法用static关键词修饰
- 静态方法属于类的,实例方法属于类的实例
- 静态方法可以使用类名直接调用,实例方法必须通过实例调用
- 静态方法只能访问静态变量和静态方法,因为实例属于变量的;实例方法都可以访问
- 静态方法随着类的加载而加载,类的卸载而消失;实例方法是通过对象的创建而存在,对象的销毁而消失。
- 静态方法可以被重载,但是不能被子类重写,因为方法绑定在编译的时候已经确定了,因此也不具有多态性。静态方法中不能使用this关键字,因为this表示当前对象实例,而静态方法属于类,不属于任何实例。
final、finally、finalize的区别
final用于修饰变量、方法、类,表示不可改变或不可继承。
final修饰的变量不能修改,变成我们常说的常量;final修饰的方法无法被子类重写;final修饰的类无法被继承。
finally用于异常处理,和try-catch配合使用,无论是否捕获异常,finally块中的代码总会执行,通常用于关闭资源,比如输入、输出流,数据库连接等。
finalize是Object类的一个方法,在对象被垃圾收集器回收之前,finalize 方法会被调用,执行清理操作(例如释放资源)。 然而,finalize 方法已经被弃用,不推荐使用,因为它不保证及时执行,并且其使用可能导致性能问题和不可预测的行为。
为什么Java中编写代码时会出现乱码问题?
涉及编码和解码,编码就是将字符按照一定的格式转换成字节流的过程,解码就是将字节流解析成字符。乱码是因为编解码时使用的字符集不一致导致的。
那为什么要需要编解码呢?
因为计算机底层的存储都是0101,它可不认识什么字符。所以我们需要告诉计算机什么数字代表什么字符。这样的一套对应规字符和字节流的对应关系就是字符集,所以编解码用的字符集不同,就乱码了。
常用字符编码
- ASCII码规定了常用字母、数字、字符和对应的数字编号(95个)
- 中国制定了GB2312字符集,后续由发布了GBK,基于GB2312增加了一些繁体字等字符,这里的K是扩展的意思。
扩展: Unicode
因为之前的编码无法统一,所以就指定了一个统一码Unicode,Unicode和之前的编码不太一样,它将字符集和编码实现解耦了。在unicode中,每一个字符保证有唯一字符码,将字符码转换成二进制字节流的过程独立出来。
什么是Java中的迭代器Iterator?
迭代器其实是一种设计模式,用于遍历集合中的元素,而不需要暴露集合的内部实现。Java中Iterator是一个接口,常用的方法有hasNext()判断是否有更多的元素可以迭代;next()返回迭代器的下一个元素,实现了接口的类就可以使用。
for和foreach的区别
for相对而言更加灵活,可以控制初始条件、终止条件、步进方式;也可以通过索引来修改数组或者集合中的元素。
foreach提供了一种更加简单的语法来遍历数组和集合,没有索引,在遍历过程中不能修改集合结构,不能添加或者删除元素。
动态代理
什么是Java中的动态代理?
动态代理用于在运行时创建代理对象,不需要在代码中提前定义具体的类。
动态代理的主要用途包括:
- 简化代码:通过代理模式,可以减少重复代码,比如一些通用行为(如日志记录、事务管理、权限控制等)方面。动态代理是实现面向切面编程的基础,可以在方法调用前后插入额外的逻辑。
- 增强灵活性:因为代理对象是在运行时生成的,可以动态地改变行为,使得代码更具灵活性和扩展性。
常见的动态代理有JDK动态代理和CGLIB。
JDK动态代理和CGLIB动态代理的区别
JDK动态代理是基于接口的,所以要求代理类一定是实现接口的。
JDK动态代理实现原理:
- 实现InvocationHandler接口,重写invoke方法得到切面类
- 利用Proxy根据目标类的类加载器、接口和切面类得到一个代理类
- 代理类的逻辑就是把接口方法的调用转发到切面类的invoke()方法中,然后进行额外的操作,通过反射调用目标类中的方法。
CGLIB使用ASM字节码处理工具,通过继承方式,转换字节码并生成新的类。不需要目标类实现接口。
注解原理是什么?
注解其实就是一个标记,可以标记在类上、方法上、变量上;有了标记之后,我们就可以在解析的时候得到这个标记,然后进行一些处理。注解的生命周期有三大类:
- RetentionPolicy.SOURCE:给编译器用的,不会写入class 文件
- RetentionPolicy.CLASS:写入class文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
- RetentionPolicy.RUNTIME:写入class 文件永久保存,可以通过反射获取注解信息
反射机制
反射其实就是Java提供的能在运行期间得到对象信息的能力,包括属性、方法、注解等。框架上使用的比较多,因为很多场景比较灵活,不确定目标对象的类型,需要通过反射来获取对象信息。比如Spring使用反射机制来读取和解析配置文件,实现依赖注入;动态代理,使用反射机制在运行的时候动态创建代理对象。
所以反射机制的优点是:
- 可以动态地获取类的信息
- 可以动态地创建对象。
- 可以动态调用对象的属性和方法,在运行时改变对象的行为。
反射是怎么实现的
- Class对象:每个类被加载后,JVM都会为其创建一个Class类型的对象,这个对象包含了该类的全部信息,如类名、包名、父类、实现的接口、所有字段、方法等。即使是基本类型、数组、枚举等也都有对应的Class对象。开发者可以通过Class.forName(String className)、对象.getClass()或类字面常量.class等方式获取到这个Class对象。
- 元数据读取:当需要进行反射操作时,Java虚拟机会读取这些Class对象中的元数据,这些元数据包括了类的各种信息。通过这些信息,程序可以在运行时动态创建对象、调用方法、访问和修改字段值,甚至可以动态改变某些访问修饰符(如通过setAccessible(true)访问私有成员)。
- 动态代理:Java反射还支持创建动态代理类,可以在运行时生成一个实现一组给定接口的新类。这在构建框架和需要在方法调用前后添加额外逻辑(如日志、事务管理)的场景中非常有用。
- 字节码操作:在更深层次上,反射操作有时会涉及字节码操作(如使用ASM、ByteBuddy等库),允许在运行时修改或生成类的结构,但这超出了标准反射API的范畴,属于更高级的动态编程技术。
什么是Java中的SPI机制?
SPI【服务提供者的接口】,SPI 机制允许服务提供者通过特定的配置文件将自己的实现注册到系统中,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。
一个典型的SPI应用场景是JDBC (Java 数据库连接库) , 不同的数据库驱动程序开发者可以使用JDBC库,然后定制自己的数据库驱动程序。
一般模块之间都是通过接口进行通讯,因此在服务调用方和服务实现方之间引入一个“接口”。
- 当实现方提供了接口和实现,我们可以通过调用实现方的接口,这就是 API。
- 当接口存在于调用方这边时,这就是 SPI 。由调用方确定接口规则,然后由不同的实现方根据这个规则对接口进行实现,从而提供服务。
SPI机制的优缺点
SPI机制可以大大提高接口设计的灵活性。
但是SPI机制存在一些缺点:
- 需要遍历加载所有的实现类,效率较低。
- 当多个ServiceLoader同时load时,会有并发问题。
泛型
什么是泛型?有什么用
使用泛型参数,可以增强代码的通用性和稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。
什么是泛型中的上下届限定符?
上界限定符是extends,下界限定符是super
<? extends T>
表示类型的上界,这个类型要么是T,要么是T的子类
<? super T>
表示类型的下界,这个类型要么是T,要么是T的父类,直到Object
泛型的使用方法有哪几种?
- 泛型类【在实例化泛型类时,必须指定泛型的类型】
- 泛型接口【实现泛型接口,可以指定泛型类型,也可以不指定】
- 泛型方法
项目中哪里使用到了泛型
- 自定义返回的结果类
Result<T>
,可以传入不同的具体参数,指定返回的结果类型。