面试Java-基础
【面试】Java 基础
1、Java 中几种基本数据类型什么,各自占用多少字节
基本数据类型 | 字节数 | 范围 |
---|---|---|
byte | 1个字节(8位) | -2^ 7~2^7-1 |
short | 2个字节(16位) | -2^ 15~2^15-1 |
int | 4个字节(32位) | -2^ 31~2^31-1 |
long | 8个字节(64位) | -2^ 63~2^63-1 |
float | 4个字节(32位) | -2^ 31~2^31-1 |
double | 8个字节(64位) | -2^ 63~2^63-1 |
char | 2个字节(16位) | -2^ 15~2^15-1 |
boolean | 1个字节(8位) | -2^ 7~2^7-1 |
2、基本数据同包装类的区别
初始值不同
:基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null。是否可以用于泛型
:包装类型可用于泛型,而基本类型不可以,泛型不能使用基本类型,因为使用基本类型时会编译出错。在栈中基本类型比包装类型更高效
:基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。是否适用new关键字
:基本类型不适用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间。
3、Java 基本类型的参数传递和引用类型的参数传递有啥区别
- 当使用基本数据类型作为方法的形参时,在方法体中对形参的修改不会影响到实参的数值。
- 当使用引用数据类型作为方法的形参时,若在方法体中修改形参指向的数据内容,则会对实参变量的数值产生影响,因为形参变量和实参变量共享同一块堆区。
- 当使用引用数据类型作为方法的形参时,若在方法体中修改形参变量的指向,此时不会对实参变量的数值产生影响,因此形参变量和实参变量分别指向不同的堆区。
4、隐式类型转换和显式类型转换
- 隐式类型转换
隐式转换也叫作自动类型转换,由系统自动完成,从存储范围小的类型到存储范围大的类型。
byte > short(char) > int > long > float > double
- 显式类型转换(+= 内部含强制转换)
显示类型转换也叫作强制类型转换,是从存储范围大的类型到存储范围小的类型。我们需要将数值范围较大的数值类型赋给数值范围较小的数值类型变量时,由于此时可能会丢失精度。
5、switch 语句表达式结果的类型
表达式结果的类型只能是byte,short,int,char,String,enum。可作用于 char byte short int 对应的包装类。
- byte、short、char可以自动提升为int类型,编译器会生成一个跳转表,直接根据表达式的值跳转到对应的case分支,因此它们也可以用于switch语句。
- 枚举类型的底层实现也是基于整数的,编译器会使用枚举的ordinal值(枚举常量的序号)来实现跳转,因此可以高效地用于switch语句。
- 虽然String是引用类型,编译器会生成hashCode的比较逻辑,并通过equals方法确保精确匹配,从而实现高效的匹配。
6、数组的扩容方式
- Arrays.copyOf(ages, ages.length+1)
- System.arraycopy(ages, 0, ages2, 0, ages.length)
7、面向对象三大特征
Java也支持面向对象的三大特征:封装、继承和多态。
- 封装
java中提供了不同的封装级别:public、protected、默认的、private。
- public:公共的,可以修饰类,成员变量,成员方法,修饰的成员在任何场景中都可以访问。
- protected:受保护的,可以修饰成员变量和方法,修饰的成员在子类和同一个包中可以访问。
- default:不加任何修饰符,类,变量,方法。
- private:私有的,可以修饰成员变量和方法。
- 继承
象现实世界中儿子可以继承父亲的财产、样貌、行为等一样,编程世界中也有继承,继承的主要目的就是为了复用。子类可以继承父类,这样就可以把父类的属性和方法继承过来。
如Dog类可以继承Animal类,继过来嘴巴、颜色等属性,吃东西、奔跑等行为。
- 多态
多态是指在父类中定义的属性和方法被子类继承之后,可以通过重写,使得父类和子类具有不同的实现,这使得同一个属性或方法在父类及其各个子类中具有不同含义。
8、静态变量和成员变量的区别
- 静态变量属于类,所以称为类变量,成员变量属于对象,所以称为对象变量。
- 静态变量储存于方法区的静态区,成员变量储存于堆内存中。
- 静态变量随着类的加载而加载随着类的消失而消失,成员变量随着对象的创建而存在随着对象的消失而消失。
- 静态变量可以通过类名调用,也可以通过对象调用,成员变量只能通过对象名调用。
9、成员变量与局部变量的区别
生命周期
:成员变量当创建对象new Cell(12,12)堆内存中为成员变量分配空间,当对象被垃圾回收,成员变量从堆内存消失。局部变量即为方法中定义的变量当方法被调用,局部变量进栈,当方法调用结束,栈区变量即出栈。初始化
:成员变量是定义在类中的,在使用之前可以不初始化,可以自动初始化值,局部变量定义在方法中不会自动初始化,成员变量在使用之前要定义赋值。使用范围
:类创建对象之后,成员变量自动进入堆内存中,成员变量在类内部都可以访问,局部变量会出现在栈内存中,局部变量只能在定义的方法内使用当方法执行结束,局部变量自动被清除。
10、& 和 && 的区别
短路与 && 第一个是 false 就是 false ,不会继续判断第二个条件,如果第一个 true 就会继续判断。与运算 & 两个条件都判断。
11、讲讲类实例化顺序
- 父类静态代码块,静态代码块之间按代码顺序执行。
- 子类静态代码块,静态代码块之间按代码顺序执行。
- 父类实例代码块,实例代码块之间按代码顺序执行。
- 父类的构造函数。
- 子类实例代码块,实例代码块之间按代码顺序执行。
- 子类的构造函数。
12、抽象类和接口的区别
- 抽象类中可以有成员变量,接口只能有静态常量。
- 抽象类只支持单继承,一个类可以实现多个接口,接口之间支持多继承。
- 抽象类中的抽象方法可以有public、protected和default这些修饰符,而接口中默认修饰符是public。不可以使用其它修饰符。
- 接口和抽象类,最明显的区别就是接口只是定义了一些方法而已,在不考虑Jva8中default方法情况下,接口中是没有实现的代码的。
13、Java 创建对象有几种方式
- 用 new 语句创对象。
- 使用反射,使用 Class.newInstance 创类对象,调用类对象构造方法 Constructor。
- 第一种,使用 Class.forName 静态方法。
- 第二种,使用 类.class 方法。
- 第三种,使用实例对象 getClass() 方法。
- 调用对象 clone 方法。
- 运用反序列化手段,调用 java.io.ObjectInputStream 对象 readObject()方法。
14、深拷贝,浅拷贝区,引用拷贝
- 引用拷贝是最简单的拷贝方式,它只是复制了对象的引用(即内存地址),而不是对象本身。这意味着新旧变量指向同一个内存地址,修改其中一个变量会影响另一个变量。
- 浅拷贝会创建一个新对象,但新对象内部的元素仍然是原对象中元素的引用。也就是说,浅拷贝只复制对象的第一层,如果对象内部包含其他对象(如列表中的列表、字典中的字典等),这些内部对象不会被复制,而是继续共享。
- 深拷贝会递归地复制对象及其内部的所有对象,创建一个完全独立的新对象。深拷贝后的对象与原对象没有任何共享的内部对象,修改其中一个对象不会影响另一个对象。
15、equals 与 == 区别
== 对于基本类型和引用类型的作用效果是不同的
- 对于基本数据类型来说,== 比较的是值。
- 对于引用数据类型来说,== 比较的是对象的内存地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等
- 类没有重写 equals()方法 ,等价于通过==比较这两个对象,使用的默认是 Object类equals()方法。
- 类重写了 equals()方法,一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
16、equals 和 hashcode 关系
- 如果两个对象 equals, hashcode 一定相等。
- 如果两个对象不 equals,他们的 hashcode 有可能相等。
- 如果两个对象 hashcode 相等,他们不一定 equals。
- 如果两个对象 hashcode 不相等,他们一定不 equals。
17、String 为什么设计成不可变的
字符串常量池的需要,JVM中专门开辟了一部分空间来存储java字符串,那就是字符串池。可以大大的节省堆空间。
String s = "abcd";
String s2 = s;
对于这个例子,s和s2都表示"abcd",所以他们会指向字符串池中的同一个字符串对象:
18、字符常量和字符串常量的区别
- 形式上,字符常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符。
- 含义上,字符常量相当于一个整型值,可以参加表达式运算,字符串常量代表一个地址值。
- 占内存大小,字符常量只占2个字节,字符串常量占若干个字节。
19、String s 与 new String 有什么区别
- String s = “abc”:
- 如果常量池中没有 “abc”,则在常量池中创建一个对象。
- 如果常量池中已有 “abc”,则直接复用常量池中的对象。
- String s = new String(“abc”):
- 如果常量池中没有 “abc”,则先在常量池中创建一个对象,然后在堆中创建一个新的对象。
- 如果常量池中已有 “abc”,则直接在堆中创建一个新的对象。
20、自动装箱与拆箱了解吗
拆箱和装箱
包装类是对基本类型的包装,所以,把基本数据类型转换成包装类的过程就是装箱;反之,把包装类转换成基本数据类型的过程就是拆箱。
自动拆装箱
在Java 5中,为了减少开发人员的工作,Java提供了自动拆箱与自动装箱功能。
- 自动装箱:就是将基本数据类型自动转换成对应的包装类。
- 自动拆箱:就是将包装类自动转换成对应的基本数据类型。
Integer i=10;//自动装箱
int b=i;//自动拆箱
自动拆装箱原理
自动装箱都是通过包装类的value0f()方法来实现的,自动拆箱都是通过包装类对象的xxxValue()来实现的。
21、包装类型的缓存机制
自动装箱的方式,其实底层用的还是valueOf方法,只是现在不用要手动执行了,是通过编译器调用,执行时会自动生成一个静态数组作为缓存,例如Integer默认对应的缓存数组范围在[-128,127],只要数据在这个范围内,就可以从缓存中拿到相应的对象。超出范围就新建对象,这个就是缓存机制。
其它类型缓存范围
Byte:(全部缓存)
Short:(-128 — 127缓存)
Integer:(-128 — 127缓存)
Long:(-128 — 127缓存)
Float:(没有缓存)
Double:(没有缓存)
Boolean:(全部缓存)
Character:(0 — 127缓存)
测试
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println(a == b); //new创建的两个对象,即使值相同,指向的内存地址也是不同的,使用==进行比较,比较的是地址,返回结果为false
// false
int c = 1;
Integer d = 1;
System.out.println(a == c); //包装数据类型和基本是数据类型比较时候会拆箱进行比较
System.out.println(c == d); //包装数据类型和基本是数据类型比较时候会拆箱进行比较
// true
// true
Integer e = 1;
System.out.println(a == e); //非new的变成指向常量池中的对象,new的变量指向堆中新建的对象
// false
Integer f = 127;
Integer g = 127;
System.out.println(f == g); //未超出缓存范围,返回结果为true
// true
Integer h = 128;
Integer i = 128;
System.out.println(h == i); //超出缓存范围,执行时会new新对象,两个对象不同,返回结果为false
// false
22、反射的概念
Java 反射(Reflection) 是 Java 提供的一种机制,允许程序在运行时动态地获取类的信息(如类名、方法、字段、构造方法等),并操作这些信息(如调用方法、访问字段、创建对象等)。反射的核心原理是通过 JVM 在运行时动态加载和操作类的元数据(metadata)。
① JDBC 数据库连接
② XML 配置模式
23、什么是泛型
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
- 为什么使用泛型
保证了类型的安全性
- 在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
消除强制转换
- 泛型的一个附带好处是,消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。
- 泛型擦除是什么
使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。如在代码中定义的 List< Object > 和 List< String >等类型,在编译之后都会变成 List。 这个过程就称为类型擦除。
24、序列化和反序列化
序列化是指把|ava对象转换为字节序列的过程,而反序列化是指把字节序列恢复为java对象的过程。
序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。
serialVersionUID 有何用途
简单概括而言, serialVersionUID 是用于在序列化和反序列化过程中进行核验的一个版本号。
如果我们不设置serialVersionUID,系统就会自动生成,自动生成有风险,就是我们类的字段类型或者长度改变,自动生成的serialVersionUID会发生变化,那么以前序列化出来的对象,反序列化的时候就会失败。
如果有些字段不想进行序列化怎么办
对于不想进行序列化的变量,使用 transient 关键字修饰。transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 transient 还有几点注意:
- transient 只能修饰变量,不能修饰类和方法。
- transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。
- static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
25、谈谈 Java 异常层次结构
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类
- Exception:程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
- Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
检查异常
- IOException:表示输入输出异常,例如文件读写、网络连接等操作时可能出现的异常。
- SQLException:表示SQL数据库访问异常,例如数据库连接错误、执行SQL语句错误等情况
- ParseException:表示解析异常,例如日期、时间等格式化解析错误。
运行异常
- NullPointerException:表示空指针异常,当尝试对一个空引用调用方法或属性时会抛出此异常。
- ArrayIndexOutOfBoundsException:表示数组下标越界异常,当访问超出数组范围的索引时会抛出此异常。
- IllegalArgumentException:表示非法参数异常,当传入的参数不符合预期时会抛出此异常。
- ClassCastException:表示类转换异常,当试图将一个对象强制转换成另一个不兼容的类型时会抛出此异常。
- ArithmeticException:表示算术运算异常,例如除以零或取模运算时分母为零时会抛出此异常。
26、throw 和 throws 的区别
throw 作用在方法内,表示抛出具体异常,throws 作用在方法的声明上,表示方法抛出异常,由调用者来进行异常处理。
27、try - catch - finally - return 执行顺序
- 如果不发生异常,不会执行 catch 部分。
- 不管有没有发生异常,finally 都会执行到。
- 即使 try 和 catch 中有 return 时,finally 仍然会执行。
- finally 部分就不要 return 了,要不然,就回不去 try 或者catch 的 return 了。
28、final、finally、finalize 区别
final可以修饰类,变量,方法,修饰的类不能被继承,修饰的变量不能重新赋值,修饰的方法不能被重写。
finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。
finalize是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize方法,让此对象处理它生前的最后事情,这个对象可以趁这个时机挣脱被回收的命运。在可达性分析算法前提下判断是否死亡,但被判断死亡后,还有生还的机会。如何自我救赎:
- 对象覆写了finalize方法,这样在被判死后才会调用此方法,才有机会做最后的救赎。
- 回收前被调用一次finalize的调用具有不确定性,只保证方法会调用,但不保证方法里的任务会被执行完(比如一个对象手脚不够利索,磨磨叽叽,还在自救的过程中,被杀死回收了)。
29、java 中 Math.round(-1.5) 等于多少呢
- round() :返回四舍五入,负5小数返回较大整数,如 -1.5 返回 -1。
- ceil() :返回小数所在两整数间较大值,如 -1.5 返回 -1.0。
- floor() :返回小数所在两整数间较小值,如 -1.5 返回 -2.0。
30、浮点数运算的时候会有精度丢失的风险
浮点数运算精度丢失代码演示:
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
为什么会出现这个问题呢?
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
就比如说十进制下的 0.2 就没办法精确转换成二进制小数:
// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...
如何解决浮点数运算的精度丢失问题
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
BigDecimal 的 equals 原理
BigDecimal bigDecimal = new BigDecimal(1);
BigDecimal bigDecimal1 = new BigDecimal(1);
System.out.println(bigDecimal.equals(bigDecimal1));// true
BigDecimal bigDecimal2 = new BigDecimal(1);
BigDecimal bigDecimal3 = new BigDecimal(1.0);
System.out.println(bigDecimal2.equals(bigDecimal3));// true
BigDecimal bigDecimal4 = new BigDecimal("1");
BigDecimal bigDecimal5 = new BigDecimal("1.0");
System.out.println(bigDecimal4.equals(bigDecimal5));// false
通过以上代码示例,我们发现,在使用BigDecimal的equals方法对1和1.0进行比较的时候,当使用int、double定义时是true,当使用String定义时是false。
原因是,equals方法和compareTo并不一样,equals方法会比较两部分内容,分别是值和标度
所以,我们以上代码定义出来的两个BigDecimal对象(bigDecimal4和bigDecimal5)的标度是不一样的,所以使用equals比较的结果就是false了。
如何比较Big Decimal
前面,我们解释了BigDecimal的equals方法,其实不只是会比较数字的值,还会对其标度进行比较。
所以,当我们使用equals方法判断判断两个数是否相等的时候,是极其严格的。
那么,如果我们只想判断两个BigDecimal的值是否相等,那么该如何判断呢?
BigDecimalr中提供了compareTo方法,这个方法就可以只比较两个数字的值,如果两个数相等,则返回0。
BigDecimal bigDecimal4 new BigDecimal("1");
BigDecimal bigDecimal5 new BigDecimal("1.0000");
System.out.println(bigDecimal4.compareTo(bigDecimal5));
31、BIO、NIO 和 AIO 的区别
- BIO:同步阻塞IO。
BIO 是同步阻塞 I/O 模型,每个连接都需要一个独立的线程来处理。当线程执行 I/O 操作时,会一直阻塞,直到操作完成。
+-------------------+ +-------------------+
| Client 1 | | Client 2 |
| (Connection) | | (Connection) |
+--------+----------+ +--------+----------+
| |
| |
+--------v----------+ +--------v----------+
| Thread 1 | | Thread 2 |
| (Blocking) | | (Blocking) |
+-------------------+ +-------------------+
- NIO:同步非阻塞IO。
NIO 是同步非阻塞 I/O 模型,基于多路复用器(Selector)实现一个线程处理多个连接。线程不会阻塞在 I/O 操作上,而是通过轮询的方式检查数据是否就绪。
+-------------------+ +-------------------+
| Client 1 | | Client 2 |
| (Connection) | | (Connection) |
+--------+----------+ +--------+----------+
| |
| |
+--------v---------------------------v----------+
| Selector |
| (Monitors multiple connections) |
+--------+---------------------------+----------+
| |
| |
+--------v----------+ +--------v----------+
| Thread 1 | | Thread 2 |
| (Non-blocking) | | (Non-blocking) |
+-------------------+ +-------------------+
- AIO:异步非阻塞IO。
AIO 是异步非阻塞 I/O 模型,线程发起 I/O 操作后立即返回,操作系统完成操作后通过回调机制通知线程。
+-------------------+ +-------------------+
| Client 1 | | Client 2 |
| (Connection) | | (Connection) |
+--------+----------+ +--------+----------+
| |
| |
+--------v---------------------------v----------+
| OS Kernel |
| (Handles I/O operations) |
+--------+---------------------------+----------+
| |
| |
+--------v----------+ +--------v----------+
| Callback 1 | | Callback 2 |
| (Notification) | | (Notification) |
+-------------------+ +-------------------+
对比总结