Java面经篇

Java基础面试题汇总

概述

说一下对Java的了解

(1)首先,Java是由sun公司开发一种面向对象的编程语言,Java语言的作为面向对象的三大特征是封装、继承、多态。

(2)封装,就是将对象的属性和行为特征包装到类中,把实现的细节隐藏起来,通过对外部提供的公共方法来展示类对外提供的功能。

(3)继承,简单来说就是对原有类的拓展,新类的定义可以增加新的数据或者新的功能,也可以使用父类的功能,但是不能选择性的继承父类,通过继承,可以快速创建新的类,提高代码的复用性。

(4)多态,表示为一个对象具有多种状态,具体表现为父类的引用指向了子类的实例。

(5)Java的基本特点有面向对象 、平台无关性 、简单性、解释执行、多线程、分布式、高性能、安全性等。

JVM、JRE、JDK

(1)JDK是Java开发工具包,是功能齐全的SDK,它包含JRE,提供了编译、运行Java程序所需要的各种工具(编译工具javac、打包工具jar)和资源,是整个Java的核心。

(2)JRE是Java运行时环境,它是运行已经编译的Java程序所需要的所有内容的集合,包含JVM以及Java核心类库。

(3)JVM是Java虚拟机,是整个Java实现跨平台的核心部分,负责解释执行字节码文件,是可运行Java字节码文件的虚拟计算机。

JDK包括JRE,JRE包含JVM。

为什么Java代码可以实现一次编译,到处运行

Java虚拟机(JVM)是Java跨平台的关键。

在程序运行之前,Java源代码(.java)需要经过编译器编译字节码(.class),在程序运行时,JVM负责将字节码翻译成特定机器下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。

Java源代码能在不同的平台上运行,它不需要做出任何改变,并且只需要编译一次。而编译好的字节码,是通过JVM实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。

注意:

  • 编译的结果是产生字节码,而不是机器码。字节码不能直接运行,必须要通过JVM翻译成机器码才能运行。
  • 跨平台的是Java程序,而不是JVM,JVM是用c/c++开发的软件,不同平台下需要安装不同的版本。

什么是字节码?使用字节码的好处是什么?

在Java中,JVM可以理解的代码就叫做字节码(即扩展名为.class文件),它不面向任何特定的处理器,只面向虚拟机。

字节码是一种中间状态的二进制代码,是由源码编译过来的,可读性没有源码高,而且cpu也不能直接读取字节码,在Java中,字节码需要经过JVM虚拟机转译成机器码之后,cpu才能够读取并运行。

好处:Java语言通过字节码的方式,在一定程序上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点,而且字节码并不转对一种特定的机器,因此,Java程序无须重新编译就可以在不同平台运行。

注:机器码就是cpu能够直接读取并且运行的代码,用二进制编码表示,也叫做机器指令码。

为什么说Java语言 “编译与解释并存”

(1)编译型:编译型语言会通过编译器将源代码一次性翻译成可被平台执行的机器码。编译语言的执行速度比较快,开发效率比较低。常见的编译型语言有c、c++、go

(2)解释型:解释型语言会通过解释器一句一句的将代码解释为机器码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释型语言有python、JavaScript

(3)因为Java语言既有编译型语言的特征,也具有解释型语言的特征。因为Java程序要先经过编译,后解释两个步骤,由Java编写的程序需要先经过编译步骤,生成字节码(.class)文件,这种字节码必须由Java解释器来解释执行。

对面向对象和面向过程的理解

(1)面向对象

面向对象就是将现实问题构建关系,然后抽象成类,给类定义属性和方法后,再将类实例化成实例,通过访问实例的属性和调用方法来解决问题。

(2)面向过程

面向过程就是一种以过程为中心的编程思想。面向过程是具体化的,把问题分解成一个个步骤,每个步骤用函数实现,需要一步一步调用解决。

(3)区别

两者主要区别在于解决问题的方式不同:

  • 面向过程把解决问题的过程拆分成一个个方法,通过一个个方法的执行解决问题。
  • 面向对象会先抽象出对象,然后用对象执行方法的方式解决。

(4)优缺点

面向对象:

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态的特性,可以设计出更加低耦合的系统,使系统更加灵活,更加易于维护。
  • 缺点:性能比面向过程差。

面向过程:

  • 优点:性能比面向对象高,因为类的调用需要实例化,性能开销比较大。
  • 缺点:没有面向对象易维护、易复用、易扩展。

面向对象的三大特征

封装

封装,就是将对象的属性和行为特征包装到类中,把实现的细节隐藏起来,通过对外部提供的公共方法来展示类对外提供的功能。Java中实现封装的关键字是private、protected和public。

封装的好处:

(1)封装提高了代码的可维护性和可重用性,因为内部实现可以随时更改而不会影响外部调用者。

(2)封装可以保护对象的状态,防止外部非法访问和修改对象的状态。

(3)封装可以控制对象的访问级别,通过公共接口来控制对象的访问和修改,实现了数据的隐藏和保护。

继承

继承,简单来说就是对原有类的拓展,一个类可以派生出一个或者多个子类,子类继承父类的属性和方法,并且可以增加自己的属性和方法,通过继承,可以快速创建新的类,提高代码的复用性。Java中实现继承的关键字是extends。

继承的好处:

(1)继承可以提高代码的可重用性和可维护性,减少重复的代码。

(2)继承可以实现类之间的层次关系,提高代码的抽象程度和灵活性.

(3)子类可以继承父类的属性和方法,同时也可以根据需要增加自己的属性和方法。

多态

多态,表示为一个对象具有多种状态,即一个对象在不同的情况下会有不同的体现。多态通过继承和接口实现的,子类可以重写父类的方法,从而使得同一个方法在不同的对象上表现出不同的行为。

多态的好处:

(1)多态可以提高代码的可扩展性,不需要修改原有的代码就可以添加新的子类。

(2)多态可以让程序编写更加灵活,由于多态的存储,可以编写适用于多个对象的代码,从而提高了代码的灵活性和可重用性。

Java和C++的区别

相同:Java和C++都是面向对象的语言,都支持封装、继承、多态。

不同:

  • Java不提供指针来直接访问内存,程序内存更加安全
  • Java的类只能是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承
  • Java有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
  • C++同时支持方法重载和操作符重载,但是Java只支持方法重载

项目中哪里体现了面向对象的特点

(1)对象的封装:将店铺、服务等核心模型封装成一个对象,通过对外提供的方法实现信息的隐藏和数据保护。

(2)继承和多态:使用继承和多态来减少重复代码,实现代码的可维护性和可扩展性。比如视频项目中电影和电视剧可以继承共同的一个父类,实现共同的属性和方法。

(3)接口定义:为了使不同对象之间更好的协同工作,我们定义了接口,实现不同对象之间的解耦,例如,定义视频接口来规范视频的所有操作。

(4)多层架构:采用多层架构的设计模式,将应用程序分为表现层、业务逻辑层和数据访问层,以达到高内聚低耦合的目的。

数据类型

介绍一下Java的数据类型

Java的数据类型包括基本数据类型和引用数据类型。

(1)基本数据类型

基本数据类型有8个,可以分为4个小类。分别是整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean)。

  • 6 种数字类型:
    • 4 种整数型:byteshortintlong
    • 2 种浮点型:floatdouble
  • 1 种字符类型:char
  • 1 种布尔型:boolean

这 8 种基本数据类型的默认值以及所占空间的大小如下:

基本类型 位数 字节 默认值 取值范围
byte 8 1 0 -128 ~ 127
short 16 2 0 -32768(-215) ~ 32767(215 - 1)
int 32 4 0 -2147483648 ~ 2147483647
long 64 8 0L -9223372036854775808(-263) ~ 9223372036854775807(263 -1)
char 16 2 ‘u0000’ 0 ~ 65535(216 - 1)
float 32 4 0f 1.4E-45 ~ 3.4028235E38
double 64 8 0d 4.9E-324 ~ 1.7976931348623157E308
boolean 1 false true、false

byteshortintlong能表示的最大正数都减 1 了。这是为什么呢?这是因为在二进制补码表示法中,最高位是用来表示符号的(0 表示正数,1 表示负数),其余位表示数值部分。所以,如果我们要表示最大的正数,我们需要把除了最高位之外的所有位都设为 1。如果我们再加 1,就会导致溢出,变成一个负数。

对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。可能为1字节或者4字节。

关于boolean类型的详细介绍,可以看以下两篇文章

腾讯面试官问我Java中boolean类型占用多少个字节?我说一个,面试官让我回家等通知 - 腾讯云开发者社区-腾讯云 (tencent.com)

java boolean 大小 - 掘金 (juejin.cn)

为什么要有包装类

Java语言是面向对象的语言,其设计理念是”一切皆对象”,但是八种基本数据类型却出现了例外,他们不具备对象的性质,正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,就是包装类。

基本类型与包装类型的区别

  • 默认值:包装类型不赋值就是null;基本类型有默认值且不是null
  • 能否用作泛型:包装类型可用于泛型,而基本类型不可以
  • 存储:基本数据类型的局部变量存放在Java虚拟机栈的局部变量表中,而成员变量(未被static修饰)存放在Java虚拟机的堆中;包装类型属于对象类型,几乎所有的对象实例都存在于堆中
  • 空间占用:相比包装类型,基本类型占用的空间非常少

注:泛型要求包容的类型是对象类型,而基本数据类型在Java中不属于对象类型,其封装类属于对象类型。

包装类型的缓存机制

Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。

另外,阿里巴巴规范中提到了:所有整型包装类对象之间值的比较,全部使用equals方法比较

解释:对于Integer在-128至127之间的赋值,Integer对象是在cache中产生,会复用已有对象,这个区间内的Integer值可以使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,推荐使用equals方法进行判断。

什么是自动装箱和拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来
  • 拆箱:将包装类型转换为基本数据类型

装箱其实是调用了包装类的valueOf()方法,拆箱其实就是调用了xxxValue()方法。

注意:如果频繁拆装箱的话,也会严重影响系统的性能,我们应该避免不必要的拆装箱操作。

类与对象

一个Java文件中可以有多个类(不含内部类)吗

一个Java文件中可以有多个类,但是最多只能有一个被public修饰。并且被public修饰的类名必须与文件名相一致。

如果该Java文件中没有public的类,则文件名可以任意,需要注意的是,当用javac指令编译有多个类的Java文件时,它会给每一个类生成一个对应的.class文件。

为什么只能有一个public类

每个编译单元(即文件)只能有一个public类,这表示每个编译单元都有唯一的公共接口,用public类来表现,只能有一个public类是为了给类加载器提供方便。

该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的public类,编译器就会给出报错信息。

编译单元里可以没有public类,指的是没有公开的接口,但是可以在同一个包里访问的。public的作用是包内包外均可访问。

方法的重载和重写的区别

重载:发生在同一个类中,方法名必须相同参数类型、参数个数、参数顺序不同,方法返回值和访问修饰符可以不同。

​ 简单来说:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。

重写:发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。

​ 如果父类方法访问修饰符为private/final/static,则子类就不能重写该方法,但是被static修饰的方法能够被再次声明。

​ 构造方法不能被重写。

方法的重写和重载是Java多态性的不同表现,重写是父类与子类之间多态性的一种变现,重载可以理解为多态的具体表现。
(1)重载是一个类中定义了多个方法名相同,但是参数的数量、类型、次序不同。
(2)重写是在子类中存在方法的方法名与父类相同,而且参数个数、类型也一样。
(3)重载是一个类的多态性的表现,而重写是子类与父类的一种多态性的表现。

成员变量和局部变量的区别

由类和方法的定义可知,在类和方法中都可以定义属于自己的变量。类中定义的变量是成员变量,而方法中定义的变量则是局部变量。类的成员变量和方法的局部变量是有一定区别的。

(1)从语法形式上看,成员变量是属于类的,而局部变量是在方法中定义的变量或者是方法的参数;

成员变量可以被public、priavte、static等修饰符所修饰,而局部变量则不能被访问控制修饰符及static修饰;

成员变量和局部变量都可以被final修饰。

(2)从变量在内存中的存储方式上看,成员变量是对象的i一部分,而对象是存在堆内存的,而局部变量是存在于栈内存的。

(3)从变量在内存中生存的时间来看,成员变量是对象的一部分,随着对象的创建而存在,而局部变量随着方法的调用而产生,随着方法调用的结束而自动消失。

(4)成员变量如果没有被赋初值,则会自动以类型默认值赋值(有一种情况例外,被final修饰但没有被staitc修饰的成员变量必须显式的赋值);而局部变量则不会自动赋值,必须被显式的赋值以后才能使用。

equals和 == 的区别

(1)equals是方法,==是操作符;

(2)对于基本类型的变量来说,只能使用 == ,因为这些基本类型的变量没有equals方法,一般来说是比较它们的值。

(3)对于引用类型的变量来说才有equals方法。对于该类型对象的比较,默认情况下,就是没有重写Object类的equals方法,使用equals和== 的比较是一样的,都是比较它们在内存中的地址;但是如果重写了equals方法,就会以实际情况来区分,比如String,使用equals方法会比较它们的值。

(4) == :对于基本类型是比较值是否相同,对于引用类型是比较内存中的地址是否相同;

equals:默认情况下,比较内存地址是否相同,可以按照逻辑需求,重写对象的equals方法。

为什么重写equals方法还要重写hashCode方法

equals方法和hashCode方法的关系:

  • 如果equals方法相等,则它们的hashCode的值一定相同,两个对象必然相等
  • 如果equals方法不等,则它们的hashCode的值不一定不同,两个对象必然不相等
  • 如果hashCode的值不等,两个对象一定不相等
  • 如果hashCode的值相等,两个对象不一定相等,要使用equals是否相等来判断两个对象是否相等

如果我们只重写equals方法,是可以的,但是在HashMap或者HashSet这种散列表中,使用equals的效率太低了,而直接比较hashCode的值效率比较高,但是hashCode的值相等并不一定就说键值对或者对象相等,所以重写eqauls必须要重写hashCode方法,一方面是为了效率,另一方面是为了确保结果的正确性。

Java的接口和抽象类区别

(1)接口

接口是Java语言中的一个抽象类型,用于定义对象的公共行为。使用关键字interface实现,在接口的实现中可以定义方法和常量,其普通方法是不能有具体代码实现的。在JDK8之后,接口中可以创建static和default方法了,并且这两种方法可以有默认的方法实现。

  • JDK8接口中可以定义static和default方法,并且这两种方法都可以包含具体的代码实现
  • 实现接口要使用implements关键字
  • 接口不能直接实例化
  • 接口中定义的变量默认是 public static final 类型
  • 子类不可以重写接口中的static和default方法,不重写的情况下,默认调用的是接口的实现方法

(2)抽象类

抽象类使用关键字abstract实现。一个方法无法给出具体明确的,该方法就可以声明为抽象类。

  • 抽象类使用abstract关键字声明
  • 抽象类中可以包含普通方法和抽象方法,抽象方法不能有具体的代码实现
  • 抽象类需要使用extends关键字来继承
  • 抽象类不能直接实例化
  • 抽象类中属性控制符无限制,可以定义private类型的属性

(3)区别

不同点:

​ (0)定义的关键字不同。接口使用interface定义,而抽象类使用abstract定义。并且子类对于接口使用的关键字是实现implement,对于抽象类使用的是继承extends。

​ (1)概念不同。接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属的关系。

​ (2)类型扩展不同。抽象类只能单继承,而接口可以多实现。

​ (3)方法访问控制符不同。接口中的成员变量只能是 publicstaticfinal类型的,不能被修改且必须要有初始值,而抽象类的成员变量默认default,可在子类中被重新定义,也可被重新赋值。

​ (4)内部方法的实现不同。接口中普通方法没有实现,但JDK8中static和default方法必须有实现;而抽象类中普通方法必须有实现,而抽象方法不能有实现。

共同点:

​ (1)都不能被实例化。(2)都可以包含抽象方法。(3)都可以有默认实现的方法(Java8中可以使用default关键字在接口中定义默认方法)。

String 不可变

String为什么不可变

什么是不可变对象?

不可变对象遵守以下几条规则:

  • 类内部所有的字段都是final修饰的
  • 类内部的所有字段都是私有的,即被private所修饰
  • 类不能被集成和拓展
  • 类不能对外提供能够修改内部状态的方法
  • 类内部的字段如果是引用,也就是说指向可变对象,那么这个引用不可被获取

String为什么不可变

(1)String保存数据的char数组被final修饰且是私有的,并且String类没有提供/暴露修改这个字符串的方法,String的方法对字符串每次操作时,都会返回一个新的字符串对象,原有的字符串不会改变。

(2)String类被final修饰导致其不能被继承,进而避免了子类破坏String的不可变。

String不可变的好处

(1)字符串常量池的要求:字符串常量池是方法区中的一个特殊的存储区。当一个字符串被创建并且该字符串已经存在于池中时,将返回现有字符串的引用,而不是创建一个新的对象。如果一个字符串是可变的,用一个引用改变字符串的值会导致其他引用的错误。

(2)缓存哈希值:字符串的哈希码在Java中经常被使用,例如在HashMap和HashSet中经常被使用,不可变保证哈希码始终相同,因此无需担心即可实现。每次使用哈希码时都无需计算。

(3)安全性考虑:String被广泛用作Java类的参数,例如网络连接、打开文件等。如果String不是不可变的,则连接或者文件将被更改,这可能会导致严重的安全威胁。

(4)线程安全问题:因为不可变对象不能改变,所以可以在多个线程之间自由共享,这消除了对于同步的要求。

String、StringBuilder、StringBuffer的区别

String是不可变字符串,而StringBuilder和StringBuffer是可变字符串。

String字符串对象是不可变对象,每次操作都会返回新的字符串,原有字符串不会被改变;StringBuilder和StringBuffer是可变字符串,他们的字符串对象可以被更改,对字符串的操作不会生成新的对象,即对同一个字符串进行修改。

StringBuilder 是线程不安全的,适用于单线程环境;而StringBuffer是线程安全的,适用于多线程环境。它们两个都适用于大量字符串拼接的场景,因为它们对于字符串的拼接操作来说,不仅效率高,而且不占用额外的空间。

什么方法能改变String

// 可以通过反射来改变String的值
String name = "xiaoao";
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] values = (char[]) field.get(name);
System.out.println(name + " " + name.hashCode());
values[0] = 'z';
System.out.println(name + " " + name.hashCode());


// 输出:
// xiaoao -759499955
// ziaoao -759499955

虽然String的字符数组声明为final,并且被private修饰,但是这个final仅仅只是让value的引用不改变,而不能让字符数组的字符不能改变。可以看到上面的代码,即时字符串的值发生了改变,但是它的hashCode仍然是一样的。

private int hash; // Default to 0

public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

为什么hashCode不会改变呢?因为在第一次调用过hashCode之后,字符串对象内通过hash这个属性缓存了hashCode的计算值,只要缓存过之后就不会再计算了,所以hashCode的值不会发生改变。

StringBuilder和StringBuffer的线程安全问题

场景:在方法的内部,一个String类型的List,将这些元素用逗号分割拼接成一个完整的字符串,然后作为方法的返回值返回,这个方法可能在多线程的环境下,然后用StringBuilder或者StringBuffer会产生线程安全问题吗

(1)使用StringBuilder在多线程情况下拼接字符串,会产生线程安全问题;而StringBuffer则不会。

(2)为什么StringBuilder会产生线程安全问题?因为StringBuilder底层的append方法调用的其实是父类的append方法。而AbstractStringBuilder的append方法中有一个字符串长度的相加的操作count += len,这个操作不是一个原子操作:

  • 首先从内存中加载count到寄存器
  • 在寄存器执行相加操作
  • 将结果写入内存

在多线程环境下,各个线程的操作的结果可能被其他线程给覆盖掉,所以,得到的结果不会是正确结果。

而且还有可能会抛出下标越界异常,这是因为StringBuilder会进行扩容操作,如果在进行扩容操作的时候,数组的长度被其他线程修改了,那么可能扩容后的数组放不下新的字符串,就会出现数组下标越界异常。

String有长度限制吗

String类型的对象是有长度限制的,String对象并不能“存储”无限制长度的字符串。关于String的长度限制要从编译时限制运行时限制两方面考虑。

(1)编译时限制。

对于String s = "abc",这样的字符串声明,字符串常量abc肯定会被放入方法区的常量池中,String长度之所以会受到限制,是因为JVM的规范对常量池有所限制。常量池中的每一种数据项都有自己的类型。Java中UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8类型表示。

CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

bytes数组就是真正存储常量数据的地方。而length就是数组可以存储的最大字节数。length的类型是u2,u2是无符号的16位整数,因此理论上允许的最大长度是216-1 = 65535。所以上面byte数组的最大长度是65535。

//65535个d,编译报错 => java: 常量字符串过长
String s = "dd..dd";

//65534个d,编译通过
String s1 = "dd..d";

起始,Javac编译器的额外限制,在Javac源代码中可以看到:

private void checkStringConstant(DiagnosticPosition var1, Object var2) {
if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
this.log.error(var1, "limit.string", new Object[0]);
++this.nerrs;
}
}

代码中可以看出,当参数类型为String,并且长度大于等于65535的时候,就会导致编译失败。

注意:String的限制并不是对字符串长度的限制,而是对字符串底层存储的限制。Java中的字符串都是使用UTF8编码的,UTF8编码使用1~4个字节来表示具体的Unicode字符。所以有的字符占1个字节,而平常使用的中文需要3个字节来存储。

//65534个a,编译通过
String s1 = "aa..a";

//21845个中文”自“,编译通过
String s2 = "好...好";

// 一个英文字母a加上21845个中文”自“,编译失败 => java: 对于常量池来说, 字符串 "a好好好好好好好好好好好好好好好好好好好..." 的 UTF8 表示过长
String s3 = "a好...好";

对于s1,一个字母a的UTF8编码占用一个字节,65534字母占用65534个字节,长度是65534,也没超过Javac的限制,所以可以编译通过。

对于s2,一个中文占用3个字节,21845个正好占用65535个字节,而且字符串长度是21845,并没有超过javac对长度的限制,所以可以编译通过。

对于s3,一个英文字母a加上21845个中文”自“占用65535个字节,超过了最常限制,编译失败。

(2)运行时限制

String运行时的限制主要体现在String的构造函数上。

// 分配一个新的String,offset参数是子数组的第一个字符的索引,count参数指定子数组的长度
public String(char value[], int offset, int count) {
...
}

上面的count值也就是字符串的最大长度。在Java中,int的最大长度是231-1。所以在运行时,String的最大长度就是231-1。

但是这个也是理论上的长度,实际长度还是要看JVM的内存。一个最大的字符串会占用(2^31-1)*2*16/8/1024/1024/1024 = 4GB,所以在最坏的情况下,一个最大的字符串需要4GB的内存,如果虚拟机不能分配这么多内存,则直接报错。

(2^31-1)*2*16/8/1024/1024/1024 = 4GB
在 Unicode 编码下,每个字符通常占用 2 个字节(16 位),即使用 2 个字节表示一个字符。(char占2个字节)
由于 1 GB = 1024 MB = 1024 KB = 1024 Bytes,所以可将该字节数转换为单位 GB

(3)总结:

String的长度是有限制的:

  • 编译期限制:字符串的UTF8编码值的字节数不能超过65535,字符串的长度不能超过65534。
  • 运行时限制:字符串的长度不能超过231 - 1,占用的内存数不能超过虚拟机能提供的最大值。

泛型

什么是泛型

Java泛型是JDK5中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如List<Person> persons = new ArrayList<>();这行代码就指明了ArrayList对象只能传入Person对象,如果传入其他类型的对象就会报错。

使用泛型的意义

(1)适用于多种数据类型执行相同的代码,即代码复用

(2)泛型中的类型在使用时指定,不需要进行强制类型转换,即类型安全,编译器会检查类型。

如何使用泛型

泛型有三种使用方式,分别为:泛型类泛型接口泛型方法

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

如何理解Java中的泛型是伪泛型

Java泛型这个特性是从JDK1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。

泛型的类型擦除的原则是:

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

List如果不用泛型会出现什么问题

如果List不使用泛型,那么默认的数据类型是Object。

(1)需要强制类型转换。在想要获取对应的元素的类型时,必须要进行强制转换。

(2)可读性较差,很可能会出现ClassCastException。在使用的时候,如果不考虑兼容,那么会出现类型转换异常。

泛型的好处

(1)类型的安全。泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在很高的程度上验证类型假设。

(2)消除强制类型转换。泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。

(3)潜在的性能收益。泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。

反射

什么是反射

反射是在运行状态中

​ 对于任意一个类,都能知道这个类的所有属性和方法;

​ 对于任意一个对象,都能够调用它的任意一个方法和属性;

这种动态获取信息以及动态调用对象的方法功能称为Java语言的反射机制。

反射机制的应用场景

(1)JDBC中,利用反射动态加载了数据库驱动程序。

(2)Web服务器中利用反射调用了Servlet的服务方法。

(3)很多框架都用到反射机制,注入属性,调用方法,比如spring,使用@Component注解就可以声明一个类为SpringBean,这就是基于反射获取类,然后再获取类上的注解,进行一定的逻辑梳理。

除此之外,除了使用new创建对象之外,还可以使用Java反射创建对象。

反射机制的优缺点

(1)优点:

  • 能够在运行时动态获取类的实例,提高灵活性
  • 可以和动态编译相结合

(2)缺点:

  • 对性能有影响,需要解析字节码,将内存中的对象进行解析,这类操作总是慢于直接执行Java代码

(3)解决方法:

  • 通过setAccessible(true)关闭JDK的安全检查来提升反射速度
  • 多次创建一个类的实例时,有缓存会快很多
  • 可以使用ReflectASM工具类,通过字节码生成的方式加快反射速度

注解

什么是注解

Annotation(注解)是JDK5开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

注解本质上就是继承了Antation接口。

注解解析的方法

注解只有被解析后才会生效,常见的解析方法有两种:

编译期直接扫描:编译器在编译Java代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。

运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value@Component)都是通过反射来进行处理的。

序列化

什么是序列化和反序列化

Java 序列化和反序列化的底层原理 - 掘金 (juejin.cn)

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

  • 序列化: 将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

要实现Java对象的序列化,只需要实现接口Serializable。

下面是序列化和反序列化常见应用场景:

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化

序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

常见的序列化方式:

序列类型 是否跨语言 优缺点
hession 支持 跨语言,序列化后体积小,速度较快
protostuff 支持 跨语言,序列化后体积小,速度快,但是需要Schema,可以动态生成
jackson 支持 跨语言,序列化后体积小,速度较快,且具有不确定性
fastjson 支持 跨语言支持较困难,序列化后体积小,速度较快,只支持java c#
kryo 支持 跨语言支持较困难,序列化后体积小,速度较快
fst 不支持 跨语言支持较困难,序列化后体积小,速度较快,兼容jdk
jdk 不支持 序列化后体积很大,速度快

不想序列化的字段

(1)transient。对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。

关于 transient 还有几点注意:

  • transient 只能修饰变量,不能修饰类和方法。
  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0

(2)static。static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

(3)默认方法。也可以通过在目标类中定义默认的writeObject()readObject()方法来实现指定的序列化字段,没有被指定的字段不会被序列化。

为什么不推荐使用JDK的序列化

(1)不支持语言调用:如果开发的时候是其他语言开发的服务就不支持JDK的序列化

(2)性能差:相比于其他序列化框架的性能更低,主要原因是序列化之后的字节数组体积大,导致传输成本加大。

(3)存在安全问题:序列化和反序列化本身并不存在问题,但是当输入的反序列化的数据可以被用户控制,那么攻击者可以通过恶意构造,让反序列化产生非预期的对象,在此过程种任意构造代码。