java基础
java基础
1.面向对象基础
1.面向对象和面向过程的区别?
面向对象和面向过程是一种软件开发思想。
两者的主要区别在于解决问题的方式不同:
- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
2.创建一个对象用什么运算符?对象实体与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)
3.对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
1 | String str1 = "hello"; |
4.方法重载和重写的区别?
同个类中的多个方法可以有相同的方法名称,但是有不同的参数列表,这就称为方法重载。参数列表又叫参数签名,包括参数的类型、参数的个数、参数的顺序,只要有一个不同就叫做参数列表不同。
重载是面向对象的一个基本特性。
方法的重写描述的是父类和子类之间的。当父类的功能无法满足子类的需求,可以在子类对方法进行重写。方法重写时, 方法名与形参列表必须一致。
如下代码,Person为父类,Student为子类,在Student中重写了dailyTask方法。
1 | public class Person { |
5.构造方法有哪些特点?是否可被 override?
构造方法特点如下:
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
1 | public class MyClass { |
6.面向对象三大特性
面向对象三大特性:封装,继承,多态
1.封装
1、封装就是将类的信息隐藏在类内部,不允许外部程序直接访问,而是通过该类的方法实现对隐藏信息的操作和访问。 良好的封装能够减少耦合。
2.继承
2、继承是从已有的类中派生出新的类,新的类继承父类的属性和行为,并能扩展新的能力,大大增加程序的重用性和易维护性。在Java中是单继承的,也就是说一个子类只有一个父类。
3.多态
多态是同一个行为具有多个不同表现形式的能力。在不修改程序代码的情况下改变程序运行时绑定的代码。实现多态的三要素:继承、重写、父类引用指向子类对象。
- 静态多态性:通过重载实现,相同的方法有不同的參数列表,可以根据参数的不同,做出不同的处理。
- 动态多态性:在子类中重写父类的方法。运行期间判断所引用对象的实际类型,根据其实际类型调用相应的方法。
举例:有一个动物类和他的2个子类猫和狗,他们都有一个名为makesound的方法,但是每种动物发出的声音是不同的。
7.接口和抽象类有什么共同点和区别?
接口和抽象类都是用于实现多态性的机制,它们在定义上有一些共同点,但也有一些关键的区别。具体分析如下:
- 共同点:
- 两者都不能直接实例化,需要由具体的子类或实现类来进行实例化。
- 都可以包含抽象方法,这些抽象方法需要由继承或实现它们的类来提供具体的实现。
- 不同点:
- 语法和声明方式:接口使用
interface
关键字进行声明,而抽象类使用abstract
关键字进行声明。 - 成员变量:接口只能定义公共静态最终字段(即常量),不能定义普通成员变量;而抽象类可以定义各种类型的成员变量。
- 构造函数:接口没有构造函数,而抽象类可以有构造函数。
- 方法实现:接口中的方法默认都是公共的抽象方法,不包含具体的实现代码;而抽象类可以包含抽象方法和非抽象方法,其中抽象方法没有具体的实现,而非抽象方法有具体的实现代码。
- 继承与实现:一个类可以实现多个接口,但只能继承一个抽象类。
- 访问修饰符:抽象方法必须为public或者protected,默认为public;接口中的方法默认为public abstract。
- 静态成员:接口中可以定义默认方法和静态方法(JDK 8及以上版本),而抽象类可以有静态代码块和静态方法。
总的来说,接口主要用于定义全局的规范,而抽象类则用于描述具有部分实现的共性。选择使用接口还是抽象类取决于具体的设计需求。
8.面向对象编程的六大原则?
- 对象单一职责:我们设计创建的对象,必须职责明确,比如商品类,里面相关的属性和方法都必须跟商品相关,不能出现订单等不相关的内容。这里的类可以是模块、类库、程序集,而不单单指类。
- 里式替换原则:子类能够完全替代父类,反之则不行。通常用于实现接口时运用。因为子类能够完全替代基(父)类,那么这样父类就拥有很多子类,在后续的程序扩展中就很容易进行扩展,程序完全不需要进行修改即可进行扩展。比如IA的实现为A,因为项目需求变更,现在需要新的实现,直接在容器注入处更换接口即可.
- 迪米特法则,也叫最小原则,或者说最小耦合。通常在设计程序或开发程序的时候,尽量要高内聚,低耦合。当两个类进行交互的时候,会产生依赖。而迪米特法则就是建议这种依赖越少越好。就像构造函数注入父类对象时一样,当需要依赖某个对象时,并不在意其内部是怎么实现的,而是在容器中注入相应的实现,既符合里式替换原则,又起到了解耦的作用。
- 开闭原则:开放扩展,封闭修改。当项目需求发生变更时,要尽可能的不去对原有的代码进行修改,而在原有的基础上进行扩展。
- 依赖倒置原则:高层模块不应该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。接口和抽象类不应该依赖于实现类,而实现类依赖接口或抽象类。
- 接口隔离原则:一个对象和另外一个对象交互的过程中,依赖的内容最小。也就是说在接口设计的时候,在遵循对象单一职责的情况下,尽量减少接口的内容
9.深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
0.什么是深拷贝和浅拷贝?
浅拷贝
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。
简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝
深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
1.浅拷贝示例代码
浅拷贝(Shallow Copy): 可以使用Object.clone()
方法进行浅拷贝。需要注意的是,要使一个类支持浅拷贝,必须实现Cloneable
接口并重写clone()
方法。
1 | class Address implements Cloneable{ |
2.深拷贝示例代码
1 | @Override |
2.Java基础知识
0.什么是值传递和引用传递?
- 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。
- 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本,并不是原对象本身,两者指向同一片内存空间。所以对引用对象进行操作会同时改变原对象。
java中不存在引用传递,只有值传递。即不存在变量a指向变量b,变量b指向对象的这种情况。
举例
1 | public static void main(String[] args) { |
1.c++和java的区别
C++和Java都是广泛使用的编程语言,但它们在多个方面存在显著差异。
- 面向对象性:Java是一种纯粹的面向对象编程语言,而C++既支持面向过程编程,也支持面向对象编程。在C++中,并非所有元素都必须是对象,你仍然可以有全局函数和全局变量。相比之下,Java中的所有代码(包括函数、变量)都必须在类中定义。
- 内存管理:C++允许程序员直接管理内存,包括使用指针和手动分配及释放内存。这提供了更高的控制能力,但也增加了出错的风险,如内存泄露和空悬指针等问题。相反,Java自动进行垃圾回收(GC),负责监控并自动回收不再被引用的对象的内存空间,简化了内存管理,但也可能导致不可预测的GC暂停时间。
- 跨平台能力:Java的一个主要优点是其跨平台能力,Java代码在任何安装了Java虚拟机(JVM)的系统上都可以运行不变。C++代码则通常需要针对每个目标平台重新编译,虽然C++代码也可以跨平台,但需要更多的工作来适应不同的系统和编译器。
- 执行速度:C++通常提供比Java更快的执行速度,因为Java代码在运行时需要通过JVM转换为机器码,这一中间步骤会引入性能开销。C++代码直接编译成机器码,因此通常执行更快,但这也意味着C++程序更接近硬件层面,给开发者更多优化的空间。
- 语言特性:C++拥有一些Java不支持的特性,例如运算符重载、多重继承和强制类型转换等。而Java设计上避免了这些可能导致混淆和错误的特性。
- 易用性和学习曲线:由于Java简化了内存管理和垃圾回收机制,通常认为Java比C++更容易学习和使用。C++的学习曲线更陡峭,因为它涉及更多底层的概念和复杂性。
综上所述,两种语言各有优势和用途。选择使用哪种语言通常取决于项目需求、开发环境和目标平台。C++适合需要高性能和紧密硬件集成的系统级应用,而Java适合快速开发跨平台的应用程序。
2.Java的特点
Java是一门面向对象的编程语言。
Java具有平台独立性和移植性。
- Java有一句口号:
Write once, run anywhere
,一次编写、到处运行。这也是Java的魅力所在。而实现这种特性的正是Java虚拟机JVM。已编译的Java程序可以在任何带有JVM的平台上运行。你可以在windows平台编写代码,然后拿到linux上运行。只要你在编写完代码后,将代码编译成.class文件,再把class文件打成Java包,这个jar包就可以在不同的平台上运行了。
Java具有稳健性。
- Java是一个强类型语言,它允许扩展编译时检查潜在类型不匹配问题的功能。Java要求显式的方法声明,它不支持C风格的隐式声明。这些严格的要求保证编译程序能捕捉调用错误,这就导致更可靠的程序。
- 异常处理是Java中使得程序更稳健的另一个特征。异常是某种类似于错误的异常条件出现的信号。使用
try/catch/finally
语句,程序员可以找到出错的处理代码,这就简化了出错处理和恢复的任务
3.Java是如何实现跨平台的?
Java是通过JVM(Java虚拟机)实现跨平台的。
JVM可以理解成一个软件,不同的平台有不同的版本。我们编写的Java代码,编译后会生成.class 文件(字节码文件)。Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码,通过JVM翻译成机器码之后才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。
只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。
因此,运行Java程序必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的翻译才能执行
4.为什么说 Java 语言“编译与解释并存”?
先看看什么是编译型语言和解释型语言。
我们可以将高级编程语言按照程序的执行方式分为两种:
- 编译型:编译型语言open in new window 会通过编译器open in new window将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
- 解释型:解释型语言open in new window会通过解释器open in new window一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
5.JDK/JRE/JVM三者的关系
JVM
英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。Java 能够跨平台运行的核心在于 JVM 。
所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。也就是说class文件并不直接与机器的操作系统交互,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。
针对不同的系统有不同的 jvm 实现,有 Linux 版本的 jvm 实现,也有Windows 版本的 jvm 实现,但是同一段代码在编译后的字节码是一样的。这就是Java能够跨平台,实现一次编写,多处运行的原因所在。
JRE
英文名称(Java Runtime Environment),就是Java 运行时环境。我们编写的Java程序必须要在JRE才能运行。它主要包含两个部分,JVM 和 Java 核心类库。
JRE是Java的运行环境,并不是一个开发环境,所以没有包含任何开发工具,如编译器和调试器等。
如果你只是想运行Java程序,而不是开发Java程序的话,那么你只需要安装JRE即可。
JDK
英文名称(Java Development Kit),就是 Java 开发工具包
可以看到,JDK目录下有个JRE,也就是JDK中已经集成了 JRE,不用单独安装JRE。
另外,JDK中还有一些好用的工具,如jinfo,jps,jstack等。
最后,总结一下JDK/JRE/JVM,他们三者的关系
JRE = JVM + Java 核心类库
JDK = JRE + Java工具 + 编译器 + 调试器
6.Java创建对象有几种方式?
Java创建对象有以下几种方式:
- 用new语句创建对象。
- 使用反射,使用Class.newInstance()创建对象。
- 调用对象的clone()方法。
- 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。
7.说说类实例化的顺序
Java中类实例化顺序:
- 静态属性,静态代码块。
- 普通属性,普通代码块。
- 构造方法。
8.Java的4种引用类型
在java1.2之后,java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用,这4种引用强度依次逐渐减弱。
- 强引用(Strong Reference):在程序代码之中普遍存在的,类似
Object obj = new Object()
这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象 - 软引用(Soft Reference):用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存才会抛出内存溢出异常
- 弱引用(Weak Reference):用来描述非必需对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- 虚引用(Phantom Reference):它是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
3.Java数据类型
1.Java的基本数据类型有哪些?每个占多少位?
- byte,8bit
- char,16bit
- short,16bit
- int,32bit
- float,32bit
- long,64bit
- double,64bit
- boolean,只有两个值:true、false,可以使⽤用 1 bit 来存储
简单类型 boolean byte char short Int long float double
二进制位数 | 1 | 8 | 16 | 16 | 32 | 64 | 32 | 64 |
包装类 | Boolean | Byte | Character | Short | Integer | Long | Float | Double |
在Java规范中,没有明确指出boolean的大小。在《Java虚拟机规范》给出了单个boolean占4个字节,和boolean数组1个字节的定义,具体 还要看虚拟机实现是否按照规范来,因此boolean占用1个字节或者4个字节都是有可能的。
2.了解Java的包装类型吗?为什么需要包装类?
Java包装类型有8种,分别是:1、Byte;2、Integer;3、Short;4、Long;5、Float;6、Double;7、Boolean;8、Character。
Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int 、double 等类型放进去的。因为集合的容器要求元素是 Object 类型。
为了让基本类型也具有对象的特征,就出现了包装类型。相当于将基本类型包装起来,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
3.自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
原理方面,自动装箱时编译器会调用包装类的valueOf
方法将原始类型的值转换成对象。而在自动拆箱的过程中,编译器则通过调用类似intValue()
、doubleValue()
这样的方法将对象转换回原始类型的值。
示例代码:
1 | Integer x = 1; // 装箱 调⽤ Integer.valueOf(1) |
4.为什么浮点数运算的时候会有精度丢失的风险?
为什么会出现这个问题呢?
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
如何解决浮点数运算的精度丢失问题?
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的。
5.包装类型的缓存机制了解么?
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
举例1
1 | Integer a = 100;//自动装箱 |
这是因为Java虚拟机(JVM)对Integer
对象进行了缓存优化。对于在-128到127之间的整数,JVM会缓存这些对象的实例,以便重复使用。因此,当我们声明a
和b
并赋值为100时,它们都指向相同的缓存对象,所以a == b
的结果为true
。
然而,对于超出-128到127范围的整数,JVM不会进行缓存优化。因此,当我们声明c
和d
并赋值为200时,它们分别创建了两个不同的对象,所以c == d
的结果为false
。
举例2
下面我们来看一个问题:下面的代码的输出结果是 true
还是 false
呢?
1 | Integer i1 = 40; |
Integer i1=40
这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40)
。因此,i1
直接使用的是缓存中的对象。而Integer i2 = new Integer(40)
会直接创建新的对象。
4.Object
1.Object常用方法有哪些?
见博客:Object常用方法 - 掘金 (juejin.cn)
Object常用方法有:toString()
、equals()
、hashCode()
、clone()
等。
toString
默认输出对象地址。可以重写toString方法,按照重写逻辑输出对象值。
equals
默认比较两个引用变量是否指向同一个对象(内存地址)。可以重写equals方法,按照属性是否相等来判断
hashCode
将与对象相关的信息映射成一个哈希值,默认的实现hashCode值是根据内存地址换算出来。
clone
Java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的。Object对象有个clone()方法,实现了对象中各个属性的复制,但它的可见范围是protected的。
1 | protected native Object clone() throws CloneNotSupportedException; |
所以实体类使用克隆的前提是:
- 实现Cloneable接口,这是一个标记接口,自身没有方法,这应该是一种约定。调用clone方法时,会判断有没有实现Cloneable接口,没有实现Cloneable的话会抛异常CloneNotSupportedException。
- 覆盖clone()方法,可见性提升为public。
getClass
返回此 Object 的运行时类,常用于java反射机制。
2.equals和引用相等==的区别
- 对于基本数据类型,==比较的是他们的值。基本数据类型没有equal方法;
- 对于复合数据类型,==比较的是它们的存放地址(是否是同一个对象)。
equals()
默认比较地址值等价于==,重写的话一般按照属性是否相等去比较。
1 | String a = new String("ab"); // a 为一个引用 |
String
中的 equals
方法是被重写过的,因为 Object
的 equals
方法是比较的对象的内存地址,而 String
的 equals
方法比较的是对象的值。
3.hashCode() 有什么用?
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
4.为什么要有 hashCode?
我们以“HashSet
如何检查重复”为例子来说明为什么要有 hashCode
?
当你把对象加入
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较,如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。但是如果发现有相同hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
其实, hashCode()
和 equals()
都是用于比较两个对象是否相等。
5.为什么重写 equals 时一定要重写 hashCode?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。这样,当用其中的一个对象作为键保存到hashMap、hashTable或hashSet中,再以另一个对象作为键值去查找他们的时候,则会查找不到。
重写equals()的例子
在这个示例中,我们创建了一个Person
类,包含name
和age
两个属性。我们重写了hashCode()
方法,首先定义一个初始值为17的变量result
,然后分别将name
和age
的哈希值乘以31并加到result
上,最后返回result
作为对象的哈希码值。这样做的目的是为了让不同的对象尽可能地产生不同的哈希码值,从而提高哈希表等数据结构的性能。
1 | public class Person { |
6.两个对象的hashCode()相同,则 equals()是否也一定为 true?
equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。- 两个对象有相同的
hashCode
值,他们也不一定是相等的(哈希碰撞)。
hashcode方法主要是用来提升对象比较的效率,先进行hashcode()的比较,如果不相同,那就不必在进行equals的比较,这样就大大减少了equals比较的次数,当比较对象的数量很大的时候能提升效率。
5.String
1.String, StringBuffer 和 StringBuilder区别
1. 可变性
- String 不可变
- StringBuffer 和 StringBuilder 可变
2. 线程安全
- String 不可变,因此是线程安全的
- StringBuilder 不是线程安全的
- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
2.String 为什么不可变?
先看看什么是不可变的对象。
如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。
接着来看Java8 String类的源码:
1 | public final class String |
从源码可以看出,String对象其实在内部就是一个个字符,存储在这个value数组里面的。
(1)value数组用final修饰,final 修饰的变量,值不能被修改。因此value不可以指向其他对象。
(2)String类内部所有的字段都是私有的,也就是被private修饰。而且String没有对外提供修改内部状态的方法,因此value数组不能改变。
所以,String是不可变的。
为什么String要设计成不可变的?
主要有以下几点原因:
- 线程安全。同一个字符串实例可以被多个线程共享,因为字符串不可变,本身就是线程安全的。
- 支持hash映射和缓存。因为String的hash值经常会使用到,比如作为 Map 的键,不可变的特性使得 hash 值也不会变,不需要重新计算。
- 出于安全考虑。网络地址URL、文件路径path、密码通常情况下都是以String类型保存,假若String不是固定不变的,将会引起各种安全隐患。比如将密码用String的类型保存,那么它将一直留在内存中,直到垃圾收集器把它清除。假如String类不是固定不变的,那么这个密码可能会被改变,导致出现安全隐患。
- 字符串常量池优化。String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。
既然我们的String是不可变的,它内部还有很多substring, replace, replaceAll这些操作的方法。这些方法好像会改变String对象?怎么解释呢?
其实不是的,我们每次调用replace等方法,其实会在堆内存中创建了一个新的对象。然后其value数组引用指向不同的对象。
3.String.equals() 和 Object.equals() 有何区别?
String
中的 equals
方法是被重写过的,比较的是 String 字符串的值是否相等。 Object
的 equals
方法是比较的对象的内存地址。
4.什么是字符串常量池?
字符串常量池 /(String Pool)是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。在创建字符串时,JVM首先会检查字符串常量池,如果该字符串已经存在池中,则返回其引用,如果不存在,则创建此字符串并放入池中,并返回其引用。
1 | // 将字符串对象”ab“的引用保存在字符串常量池中 |
5.String s = new String(“abc”)会创建几个对象?
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
示例代码(JDK 1.8):
1 | String s1 = new String("abc"); |
2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
示例代码(JDK 1.8):
1 | // 字符串常量池中已存在字符串对象“abc”的引用 |
6.String 类的常用方法有哪些?
- indexOf():返回指定字符的索引。
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():分割字符串,返回一个分割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比较。
7.为何JDK9要将String的底层实现由char[]改成byte[]?
主要是为了节约String占用的内存。
在大部分Java程序的堆内存中,String占用的空间最大,并且绝大多数String只有Latin-1字符,这些Latin-1字符只需要1个字节就够了。
而在JDK9之前,JVM因为String使用char数组存储,每个char占2个字节,所以即使字符串只需要1字节,它也要按照2字节进行分配,浪费了一半的内存空间。
到了JDK9之后,对于每个字符串,会先判断它是不是只有Latin-1字符,如果是,就按照1字节的规格进行分配内存,如果不是,就按照2字节的规格进行分配,这样便提高了内存使用率,同时GC次数也会减少,提升效率。
不过Latin-1编码集支持的字符有限,比如不支持中文字符,因此对于中文字符串,用的是UTF16编码(两个字节),所以用byte[]和char[]实现没什么区别。
6.异常
1.Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。- **
Error
**:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
2.运行时异常(RuntimeException)和非运行时异常(checkedExecption)的区别?
unchecked exception
包括RuntimeException
和Error
类,其他所有异常称为检查(checked)异常。
RuntimeException
由程序错误导致,应该修正程序避免这类异常发生。Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。checked Exception
由具体的环境(读取的文件不存在或文件为空或sql异常)导致的异常。必须进行处理,不然编译不通过,可以catch或者throws。
3.Throwable 类常用方法有哪些?
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
4.try-catch-finally 如何使用?
try
块:用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。catch
块:用于处理 try 捕获到的异常。finally
块:无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。
5.Java中的finally一定会被执行吗?
答案是不一定。
有以下两种情况finally不会被执行:
- 程序未执行到try代码块
- 如果当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。
6.throw和throws的区别?
(1)throws用于方法头,表示的只是异常的申明,而throw用于方法内部,抛出的是异常对象
(2)throws可以一次性抛出多个异常,而throw只能一个 (3)throws抛出异常时,它的上级(调用者)也要申明抛出异常或者捕获,不然编译报错。而throw的话,可以不申明或不捕获(这是非常不负责任的方式)但编译器不会报错。
7.泛型
Java泛型是JDK 5中引⼊的⼀个新特性, 允许在定义类和接口的时候使⽤类型参数。声明的类型参数在使⽤时⽤具体的类型来替换。
泛型最⼤的好处是可以提⾼代码的复⽤性。以List接口为例,我们可以将String、 Integer等类型放⼊List中, 如不⽤泛型, 存放String类型要写⼀个List接口, 存放Integer要写另外⼀个List接口, 泛型可以很好的解决这个问题
8.反射
见博客:详解反射机制和常见应用场景 - 掘金 (juejin.cn)
1.什么是反射
动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。 通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
2.反射应用场景
正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
Java 中的一大利器 注解 的实现也用到了反射。 为什么你使用 Spring 的时候 ,一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value
注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
3.反射的优缺点?
反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
反射的优点:
- 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
- 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
- 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
反射的缺点:
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
- 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
- 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
- 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
9.代理
代理模式是一种比较好理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
1.静态代理
静态代理:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。
静态代理实现步骤:
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情
静态代理优缺点:
优点:
通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。静态代理实现简单,且不侵入原代码。
缺点
1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
- 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
- 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
2.动态代理
相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
Spring
的AOP
使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
1.JDK动态代理
如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler
接口和Proxy
类。
缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。
2.CGLIB动态代理
通过继承实现。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。
优点:目标类不需要实现特定的接口,更加灵活。
缺点:CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final
,那么它是无法使用CGLIB做动态代理的。
3.静态代理和动态代理的对比)
- 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
9.序列化与反序列化
1.什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
2.常见应用场景
下面是序列化和反序列化常见应用场景:
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
3.如果有些字段不想进行序列化怎么办?(transient关键字)
Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。
也就是说被transient修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。
4.实现序列化和反序列化为什么要实现 Serializable 接口?
一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才能被序列化。
如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable
接口的话,在序列化时就会抛出NotSerializableException
异常!Serializable
接口也仅仅只是做一个标记用!它告诉代码只要是实现了Serializable
接口的类都是可以被序列化的!
5.static 属性为什么不会被序列化?
因为序列化是针对对象而言的,而 static 属性优先于对象存在,随着类的加载而加载,所以不会被序列化.
看到这个结论,是不是有人会问,serialVersionUID 也被 static 修饰,为什么 serialVersionUID 会被序列化? 其实 serialVersionUID 属性并没有被序列化,JVM 在序列化对象时会自动生成一个 serialVersionUID,然后将我们显示指定的 serialVersionUID 属性值赋给自动生成的 serialVersionUID
10.I/O流
1.Java IO 流了解吗?
IO 即 Input/Output
,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
2.I/O 流为什么要分为字节流和字符流呢?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
首先明确字节流适用于任何场景,而且有字节缓冲流,能提高读取和输入的效率,也就是BufferedOutputStream/BufferedInputStream。其操作与字节流基本都一样。 而字符流是为了应对汉字出现的情况。在GBK中汉字占2个字节,在UTF-8中汉字占3个字节,所以我们通过字节流读取文件的时候一般都是逐个字节转换就会导致乱码,而手动去根据不同编码去拼接则不方便,所以有字符流。
3.I/O模型
见计网操作系统笔记
11.常见的关键字
abstract和interface
Java 中的抽象类(abstract class)和接口(interface)是两种常见的抽象化机制。
抽象类是指不能直接实例化的类,只能被用来派生其他类,它被设计成为仅包含可继承的方法、属性和变量。抽象类通常用于在类层次结构的根部建立一个适当的上下文语境。常见的抽象类特征如下:
- 抽象类可以包含成员变量和成员方法,也可以包含抽象方法以及非抽象方法。
- 抽象类必须通过关键字 abstract 进行声明,并且如果类中有一个或多个抽象方法,则该类必须被声明为抽象类。
- 抽象类可以被用来给其他类作为父类,抽象类的子类需要实现其中的所有抽象方法,否则子类也必须声明为抽象类。
接口和抽象类一样也是一种特殊类型的类,它仅声明了一组或者多组方法以及常量,可以被看作是一个对外公开的 API 契约。接口在 Java 中属于比抽象类更加抽象的概念。常见的接口特征如下:
- 接口中只能包含常量、方法的声明(而非实现)以及内部定义的其他类型(如枚举类型或内部类)。
- 在接口中声明方法时必须使用关键字 public 或者 default 修饰,并且通常不需要使用 abstract 关键词,因为接口中所有方法都默认为抽象方法。
- 一个类可以实现多个接口,从而得到多个抽象函数的实现,表示它强制要求 Java 类实现该接口的相关方法。
- 除了 java.lang.Object 之外,任何类都可以实现一个接口,而无需拓展任何类。
- 接口中只有常量,没有变量。声明一个常量时必须使用 static 关键字,一般再加上 final 关键字使其成为常量
final
- 基本数据类型用final修饰,则不能修改,是常量;对象引用用final修饰,则引用只能指向该对象,不能指向别的对象,但是对象本身可以修改。
- final修饰的方法不能被子类重写
- final修饰的类不能被继承。
this
this.属性名称
指访问类中的成员变量,可以用来区分成员变量和局部变量。
super
super 关键字用于在子类中访问父类的变量和方法。
12.Java8的新特性有哪些?
- Lambda 表达式:Lambda允许把函数作为一个方法的参数
- Stream API :新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中
- 默认方法:默认方法就是一个在接口里面有了一个实现的方法。
- Optional 类 :Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
- Date Time API :加强对日期与时间的处理。
13.Java19新特性虚拟线程
1.什么是虚拟线程?
虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。
2.虚拟线程有什么优点和缺点?
优点
- 非常轻量级:可以在单个线程中创建成百上千个虚拟线程而不会导致过多的线程创建和上下文切换。
- 简化异步编程: 虚拟线程可以简化异步编程,使代码更易于理解和维护。它可以将异步代码编写得更像同步代码,避免了回调地狱(Callback Hell)。
- 减少资源开销: 相比于操作系统线程,虚拟线程的资源开销更小。本质上是提高了线程的执行效率,从而减少线程资源的创建和上下文切换。
缺点
- 不适用于计算密集型任务: 虚拟线程适用于 I/O 密集型任务,但不适用于计算密集型任务,因为密集型计算始终需要 CPU 资源作为支持。
- 依赖于语言或库的支持: 协程需要编程语言或库提供支持。不是所有编程语言都原生支持协程。比如 Java 实现的虚拟线程。
3.虚拟线程的创建方法
- 使用
Thread.startVirtualThread()
创建 - 使用
Thread.ofVirtual()
创建 - 使用
ThreadFactory
创建