本文主要是介绍Java八股文(自总)——Java基础(更新中。。。),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Java基础
- 数据类型
- 1. Java中有哪8中基本数据类型?它们的默认值和占用空间大小知道不?说说这8中基本数据类型对应的包装类型
- 2. 基本类型和包装类的区别
- 3. 包装类型常量池技术了解吗?
- 4. 为什么要有包装类型?
- 5. 什么是自动拆装箱?原理?
- 6. 遇到过自动拆箱引发的NPE问题吗?
- 面向对象
- 1. String为什么是不可变的?
- 2. String . StringBuffer , StringBuilder的区别是什么
- 3. 重载和重写的区别
- 4. 内部类了解吗? 匿名内部类了解吗?
- 5. 深拷贝和浅拷贝了解吗? 什么是引用拷贝?
- 6. 接口和抽象类有什么共同点和区别?如何选择?
- 反射 , 注解 , 泛型
- 1. Java反射? 反射有什么优点/缺点? 你是怎么理解反射的(为什么框架需要反射)?
- 2. 谈谈对Java注解的理解 , 解决了什么问题?
- 3. Java泛型了解么?泛型的作用?什么是类型擦除?泛型有哪些限制?介绍一下常用的通配符?
- (1)什么是泛型?有什么作用?
- (2) 什么是类型擦除(泛型擦除机制)? 为什么要擦除?
- (3) 泛型有哪些限制?为什么?
- (4) 为什么要用泛型 , 用`Object`不行吗?
- (4) 介绍一下常用的通配符
数据类型
1. Java中有哪8中基本数据类型?它们的默认值和占用空间大小知道不?说说这8中基本数据类型对应的包装类型
java中的8中基本数据类型
- 6种数字类型:byte、short、int、long、float、double
- 1中字符类型:char
- 1中布尔类型:boolean
默认值和占用空间
1B=8bit
char占2字节 所以一个char类型可以存储一个汉字
注意:
- Java里使用
long
类型的数据一定要在数值后面加上L
, 否则将作为整型解析 char a = 'h'
char : 单引号 ,String a = "hello"
String : 双引号`
基本类型 | 字节(byte/Byte) | 位数(bit) | 默认值 |
---|---|---|---|
byte | 1 | 8 | 0 |
short | 2 | 16 | 0 |
int | 4 | 32 | 0 |
long | 8 | 64 | 0L |
float | 4 | 32 | 0f |
double | 8 | 64 | 0d |
char | 2 | 16 | ‘u0000’ |
boolean | 1 | false |
对应的包装类型(8种)
- Byte , Short , Integer , Long , Float , Double
- Character
- Boolean
2. 基本类型和包装类的区别
存储方式 :
- 基本数据类型的局部变量存放在Java虚拟机(JVM)栈中的局部变量表中 , 基本数据类型的成员变量(未被static修饰)存放在Java虚拟机(JVM)的堆中.
- 包装类型属于对象类型 , 我们知道几乎所有对象都存在于堆中
//AI.tips
- 基本类型 : Java中的基本类型 直接存储在栈上 , 是直接存储实际值的类型
- 包装类型 : Java中的包装类型 它们是对象 , 存储在堆上 , 包含基本类型的值
用途 :
- 除了定义一些常量和局部变量之外 , 我们在其他地方比如 : 方法参数 , 对象属性中很少会使用基本类型来定义变量.
- 并且 , 包装类可用于泛型 , 而基本类型不可以
//AI.tips
- 基本类型不能直接作为集合(如List , Set , Map)的元素 , 因为集合框架只能存储对象
- 包装类型可以作为集合的元素 , 因为它们是对象
占用空间
- 肯定是基本数据类型占用的空间要小上很多
- 但从性能上来说 :
- 基本类型由于直接存储在栈上 , 访问速度更快 , 性能更好
- 包装类型作为对象存储在堆上 , 访问是需要额外的内存分配和回收 , 性能相对较低
默认值
- 基本类型 有默认值 但是在声明式必须初始化 , 否则会编译错误
- 包装类型可以不初始化 , 默认值为null
比较方式
- 基本类型 :
==
比较的是值- 包装类型 :
==
对于包装类型来说 , 比较的是对象的内存地址 . 所有整形包装类对象之间的值的比较 , 全部使用equals()
方法
3. 包装类型常量池技术了解吗?
- Java基本类型的包装类大部分都实现了常量池技术(Float,Double没有实现)
- Byte , Short , Integer , Long这4种包装类默认创建了数值[-128,127]的相应类型的缓存数据
- Character创建了数值在[0,127]范围的缓存十数据
- Boolean直接返回
True
orFalse
//我对与常量池的简单理解
常量池相当于一个池子 , 包装类型中除了float和double , 都有实现常量池技术 ,
然后这个池子有一个范围 , 如果我定义的常量属于这个范围 , 那么它们都属于常量池中这个数 ,
所以判断"=="会输出true.
而如果超出这个范围,就会创建新的对象 , 所以'=='输出为false
了解这一机制有助于优化性能和内存使用
缓存池的范围和实现可能因JVM实现而异 , 但通常-128~127是最常见的范围
举个例子
Integer a1 = 33;
Integer a2 = 33;
System.out.println(a1 == a2); //输出true
Float a11 = 333f;
Float a22 = 333f;
System.out.println(a11 == a22); //输出false
Double a3 = 1.2;
Double a4 = 1.2;
System.out.println(a3 == a4); //输出false
//来看另一个例子
Integer a1 = 40;
Integer a2 = new Integer(40);
System.out.println(a1 == a2); //输出:false
//为什么是false?
Integer a1 = 40
这一行代码会发生装箱 , 也就是说这行代码等价于 Integer a1 = Integer.valueOf(40)
因此 , a1
直接使用的是常量池中的对象 , 而Integer a2 = new Integer(40)
则会直接创建新的对象
4. 为什么要有包装类型?
Java本身就是一门OOP(面向对象编程)语言 , 对象可以说是Java的灵魂
除了定义一些常量和局部变量之外 , 我们在其他地方 比如方法参数 , 对象属性中很少会使用基本数据类型来定义变量
5. 什么是自动拆装箱?原理?
什么是自动拆装箱 :
自动拆装箱 是基本类型和包装类型之间的互转
Integer i = 10; //装箱
int n = i; //拆箱
原理 :
从字节码中 , 我们发现 :
装箱其实就是调用了 包装类的valueOf()
方法
拆箱其实就是调用了xxxValue()
方法**
因此 ,
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
6. 遇到过自动拆箱引发的NPE问题吗?
- 数据库的查询结构可能是null , 因为自动拆箱 , 用基本数据类型接受有NPE风险 NPE(NullPointerException报错) , 也就是null值问题
public class AutoBoxTest{@Testvoid should_Throw_NullPointerExce(){long id = getNum();}public Long getNum(){return null;}
}
结果 : java.lang.NullPointerException
错误
反编译.class文件
0 aload_0
1 invokevirtual #2 <AutoBoxTest.getNum>
4 invokevirtual #3 <java/lang/Long.longValue>
7 lstore_1
8 return
我们可以看到
4 invokevirtual #3 <java/lang/Long.longValue>
调用了longValue方法 , 也就是进行了自动拆箱
这里的
long id = getNum();
其实等效于
long id = getNum().longValue()
那么 , 为什么会报NPE错误呢?
因为 , getNum()方法返回了一个Long类型的null值
而null不能被正常的自动拆箱 , 所以 long id 被赋值为null
- 三元运算符使用不当会导致诡异的NPE异常
public class Main{public static void main(String[] args){Integer i = null;Boolean flag = false;System.out.printlb(flag ? 0 : i);// i进行自动拆箱==>但因为i==null 所以会有NPE异常//但如果flag初始化为true , 则不会报错===>因为会直接返回0 , 不会进行拆箱操作}
}
面向对象
1. String为什么是不可变的?
String为什么是不可变的?(两个方向–>怎么实现的不可变?被设计为不可变的原因?)
- 怎么实现的?
- 在String的源代码中 , 很多变量都是被final修饰的,像value就是
- String类没有提供任何修改其内部字符数组的方法. 如substring,replace,concat等方法都是返回新的字符串实例.
而对于final :
对于一个基本类型变量 , 如果用final修饰 , 那么就不能再对这个基本变量进行赋值
但对于一个引用类型变量而言 , final只是保证这个引用变量所引用的地址不会改变 , 即一直引用同一个对象 , 但这个对象完全可以发生改变.
例如 : 某个指向数组的final引用 , 它必须从始至终指向初始化时指向的数组 , 但是这个数组的内容完全可以改变
//基本数据类型
String s1 = "hello";
String s2 = "world";
System.out.println("s1的内存地址:"+System.identityHashCode(s1));
System.out.println("s2的内存地址:"+System.identityHashCode(s2));
System.out.println("s1+s2的内存地址:"+System.identityHashCode(s1+s2));//输出
s1的内存地址:366712642
s2的内存地址:1829164700
s1+s2的内存地址:2018699554
//引用类型—数组
@Testpublic void Test(){final int[] a = {1,2,3,4};System.out.println(a); //输出: [I@79d82f66a[1] = 0;System.out.println(a);//输出: [I@79d82f66}
- 被设计成不可变的原因:
- 常量池
在Java中 , 存在一个字符串常量池 , 它是存储字符串对象的特殊内存区域. 当我们创建一个字符串时 ,
如果该字符串已经存在于字符串常量池中 , 那么就会直接返回这个字符串的引用
如果不存在 , 则会将该字符串添加到字符串常量池中 , 并返回新创建的字符串的引用
由于字符串常量池的存在 , 多个字符串可以共享一个实例 , 这样可以节省内存空间
- 安全性
由于String是不可变的 , 所以它在多线程环境下是安全的 .
多个线程可以同时访问和共享同一个字符串对象 , 而无需担心数据的修改问题
- 哈希表键
不可变字符串可以安全地用作哈希表的键 , 因为它们的hashCode值在对象的生命周期内不会改变 . 这对于提高哈希表的性能至关重要.
- 性能优化
由于String是不可变的 , 所以可以进行一些性能优化.
例如 : 在字符串拼接时 , 如果使用StringBuilder或StringBuffer来处理可变字符串 , 会比直接修改String对象的方式更高效
2. String . StringBuffer , StringBuilder的区别是什么
从可变性来看:
String
是不可变的 . 一旦创建 , 它的值就不能被改变 ( 对String的任何修改都会生成一个新的String对象 )StringBuffer
是可变的 , 可以对StringBuffer对象进行修改 , 比如追加(append)或插入(insert)操作 , 而不需要创建新的对选哪个StringBuilder
: 也是可变的 , 与StringBuffer类似 , 可以进行修改操作
从线程安全性来看
String
由于其不可变性 , 自然具有线程安全性 , 可以在多线程环境中安全使用StringBuffer
是线程安全的 , 它的方法通常是同步的(synchronized) , 这意味着在多线程环境中 , 一次只有一个线程可以执行这些方法StringBuilder
不是线程安全的 , 它的方法不是同步的 , 因此在单线程环境中性能更好 , 但在多线程环境中使用需要额外的同步控制
从性能来看
String
: 性能最差 由于其不可变性 , 每次修改都会创建一个新的对象 , 这可能会导致较高的内存消耗和性能开销 , 特别是在频繁修改字符串的情况下StringBuffer
: 由于其线程安全 , 每次方法调用都会有同步开销 , 因此在单线程环境中可能比StringBuilder慢 , 但比String快StringBuilder
: 最好 , 在单线程环境中 , 由于没有同步开销 , 通常比StringBuffer有更好的性能
3. 重载和重写的区别
//可以从以下几点来看 :
发生范围
- 重载 : 可以发生在同一个类中 , 也可以发生在继承体系中的子类和父类之间(通常提到重载 , 我们就会想到构造函数的重载 , 这也是最常用的重载)
- 重写 : 只能是子类重写父类方法
参数列表
- 重载 : 参数列表必不能相同 , 即参数的数量 , 类型 或 顺序至少有一项不同
- 重写 : 参数列表必须与父类方法完全相同
返回值类型
- 重载 : 没要求
- 重写 : 返回值类型必须与被重写的父类方法相同 , 或者其子类型
异常
- 重载 : 没要求
- 重写 : 可以抛出相同的异常类型或更少的异常类型 , 但不能抛出更宽泛的异常类型
访问修饰符
- 重载 : 没要求
- 重写 : 访问修饰符不能比父类方法更严格 , 即不能降低访问级别
发生阶段
- 重载 : 编译时多态 , 编译器根据方法名和参数列表确定调用哪个重载版本的方法
- 重写 : 运行时多态 , 虚拟机根据对象的实际类型调用相应的重写方法
4. 内部类了解吗? 匿名内部类了解吗?
Java中的内部类有4种 : 成员内部类 , 静态内部类 , 方法内部类 , 匿名内部类
- 成员内部类
就是内部类作为一个成员 , 存在于类中 . 实例化方法 : 外部类.内部类 内部类对象 = new 外部类().new 内部类();- 静态内部类
在成员内部类的基础山 , 多了一个static关键字 , 是静态的类 , 所有的对象都可以直接通过类名调用 实例化方法 : 内部类 内部类对象 = new new 内部类();- 方法内部类
在类的方法中 , 定义内部类- 匿名内部类
直接new一个没有名字的类 , 并且直接调用其中的方法
5. 深拷贝和浅拷贝了解吗? 什么是引用拷贝?
前情 :
Java的数据类型中 , 除了基本类型(byte , short , int , long , float , double , char , boolean) 就是 引用类型(如 : 数组 , 字符串 , 类 , 接口 , 等)
- java将内存分为 栈 和 堆
- 对于基本数据类型而言 , 它们全都存放在栈中
- 对于引用类型而言 , 它们的引用存放在 栈 中 , 实际存储的值在 堆 中 (而栈中存放的引用 , 会指向堆中存储的值的地址)
拷贝一般分为两大类 : 引用拷贝 和 对象拷贝 , 而我们通常讲的深拷贝和浅拷贝都是对象拷贝
- 浅拷贝
浅拷贝会再堆上创建一个新的对象 , 不过,如果原对象内部的属性是引用类型的话 , 浅拷贝会直接复制内部对象的引用地址 , 也就是说拷贝对象和源对象共用同一个内部对象 . - 深拷贝
深拷贝会完全复制整个对象 , 包括这个对象所包含的内部对象
深拷贝和浅拷贝是用来描述对象或者对象数组中引用数据类型的一个复制场景
浅拷贝就是只复制某个对象的指针 , 而不是复制这个对象本身 , 那么这种复制方式就意味着 , 两个引用对象的指针 , 指向被复制对象的同一块内存地址
深拷贝就是创建一个完全相同的对象 , 新对象和老对象之间不共享任何内存 , 也就意味着 对新对象的修改不会影响老对象的值
在Java中不论是深拷贝还是浅拷贝都需要实现Cloneable接口 , 并且实现clone()方法
实现深拷贝 : 可以重写clone()方法的内部逻辑 , 对clone里面的内部引用变量再进行一次克隆
什么是引用拷贝?
顾名思义 , 就是对引用地址的拷贝 , 不会创建一个新的对象 , 而且拷贝对象和被拷贝对象 共享同一内存地址
浅拷贝的实现
public class Student implements Cloneable {public Address address;@Overrideprotected Object clone() throws CloneNotSupportedException {Student student = (Student) super.clone();student.setAddress((Address) student.getAddress().clone());return student;}@Overridepublic String toString() {return "Student{" +"address=" + address +'}';}public void setAddress(Address address) {this.address = address;}public Address getAddress() {return address;}public static void main(String[] args) throws CloneNotSupportedException {Student student = new Student();student.setAddress(new Address());Student clone = (Student) student.clone();System.out.println(student);System.out.println(clone);System.out.println(student == clone);/*** 输出:* Student{address=com.example.springbootpro.DeepCopy.Address@6f539caf}* Student{address=com.example.springbootpro.DeepCopy.Address@6f539caf}*/}
}public class Address {}
深拷贝的实现
public class Student implements Cloneable {public Address address;@Overrideprotected Object clone() throws CloneNotSupportedException {Student student = (Student) super.clone();student.setAddress((Address) student.getAddress().clone());return student;}@Overridepublic String toString() {return "Student{" +"address=" + address +'}';}public void setAddress(Address address) {this.address = address;}public Address getAddress() {return address;}public static void main(String[] args) throws CloneNotSupportedException {Student student = new Student();student.setAddress(new Address());Student clone = (Student) student.clone();System.out.println(student);System.out.println(clone);System.out.println(student == clone);/*** 输出:* Student{address=com.example.springbootpro.DeepCopy.Address@6f539caf}* Student{address=com.example.springbootpro.DeepCopy.Address@79fc0f2f}* false*/}
}public class Address implements Cloneable{@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}
6. 接口和抽象类有什么共同点和区别?如何选择?
共同点和区别 :
- 抽象类和接口最明显的区别 : 抽象类可以存在普通成员函数(即可以存在实现了的方法) , 而接口中的方法时不能实现的
- 接口设计的目的 : 是对类的行为进行约束 , 约束了行为的有无 , 但不对如何实现进行限制
- 抽象类的设计目的 : 是代码复用 , 将不同类的相同行为抽象出来 , 抽象类不可以实例化
如何选择:
个人认为抽象类的功能其实是包含了接口的功能 , 也就是抽象类功能更多
如何选择的话 , 可以从这个方面考虑 , Java是 单继承 , 多实现 的 , 因此继承抽象类的话 , 只能继承1个 , 而实现接口可以实现多个 , 因此在工程设计中 , 如果只是需要规定一个实现类需要实现那些方法 , 这个功能对于接口和抽象类都有 , 那么优先选择接口
而如果 , 需要使用模板方法模式 , 也就是要定义一些方法的执行流程 , 并且将这些流程延迟到子类实现 , 那么只能使用抽象类 , 因为接口不具备该功能
反射 , 注解 , 泛型
1. Java反射? 反射有什么优点/缺点? 你是怎么理解反射的(为什么框架需要反射)?
java反射
一句话总结 : 反射就是在运行时 才知道要操作的类是什么 , 并且可以在运行时 获取类的完整构造 , 并调用对应的方法
优/缺点
优点 :
- 增加程序的灵活性 , 可以在运行的过程中动态对类进行修改和操作
- 为各种框架提供了开箱即用的便利
缺点:
- 让我们在运行时有了分析操作类的能力 , 这同样也增加了安全性问题 . 比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时) . 另外 , 反射的性能也要稍微差点 , 但是 , 对于框架来说实际是影响不大的 .
为什么框架需要反射
反射之所以被称为框架的灵魂 , 主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力
通过反射你可以获取任意一个类的所有属性和方法 , 你还可以调用这些方法和属性
2. 谈谈对Java注解的理解 , 解决了什么问题?
- 注解 可以看做是一种特殊的注释 , 主要用于修饰类,方法或者变量 , 提供某些信息供程序在编译或运行时使用
- 注解本质是一个集成了
Annotation
的特殊接口- 注解只有被解析之后才会生效 , 常见的解析方法有两种:
- 编译时直接扫描 : 编译器在编译Java代码的时候扫描对应的注解并处理 , 比如某个方法使用
@Override
注解 , 编译器在编译的时候就会监测当前的方法是否重写了父类对应的方法- 运行期通过反射处理 : 像框架中自带的注解(比如Spring框架的@Value , @Component)都是通过反射来进行处理的
3. Java泛型了解么?泛型的作用?什么是类型擦除?泛型有哪些限制?介绍一下常用的通配符?
(1)什么是泛型?有什么作用?
- Java泛型是JDK5中引入的一个新特性 . 使用泛型参数 , 可以增强代码的可读性和稳定性
- 编译器可以对泛型参数进行监测 , 并且通过泛型参数可以指定传入的对象类型 . 比如
ArrayList<Person> persons = new ArrayList<person>()
这行代码就指明了该ArrayList
对象只能传入Person
对象 , 如果传入其他类型的对象就会报错.
(2) 什么是类型擦除(泛型擦除机制)? 为什么要擦除?
- Java的泛型是伪泛型 , 这是因为Java在编译期间 , 所有的泛型信息都会被擦掉 , 这也就是通常所说的类型擦除
- 为什么要擦除 ? 因为泛型是在java1.5之后引入的 , 为了能够兼容之前版本的代码 , 才要进行泛型擦除
(3) 泛型有哪些限制?为什么?
泛型的限制一般是由泛型擦除机制导致的 . 擦除为
Object
后无法进行类型判断
比如 :
- 只能声明不能实例化
T
类型变量- 不能实例化泛型数组
- 不能实例化泛型参数的数组 , 擦除后为
Object
后无法进行类型判断- 不能实现两个不同泛型参数的同一接口 , 擦除后多个父类的桥方法将冲突
- 不能使用static修饰泛型变量
(4) 为什么要用泛型 , 用Object
不行吗?
其实在Java中所有类型都是Object的子类型 , 用Object去定义当然没有问题 , 毕竟泛型在编译时也会进行泛型擦除 , 其中一部分泛型也会被转为Object类型
用Object , 虽然写代码的时候不会报错 , 但你需要时刻注意你的逻辑 比如:Cat cat = new Cat(); Dog dog = new Dog(); //泛型 ArrayList<Cat> list = new ArrayList(); list.add(cat); list.add(dog); //报错 //Object ArrayList<Object> list = new ArrayList(); list.add(cat); list.add(dog); //不会报错 ⇒ 但编译时会报错
(4) 介绍一下常用的通配符
PECS producer extends consumer super 原则
<? extends Person> //限制类型为 Manager 的父类 <? super Manager>
//限制类型为 Person 的子类
//无边界通配符
void test(Person<?> p){…}
//无边界通配符可以接收任何泛型类型的数据
这篇关于Java八股文(自总)——Java基础(更新中。。。)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!