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

  • 存放方式:基本数据类型的局部变量存放在Java虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被static修饰)存放在Java虚拟机中的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
  • 占用空间:基本数据类型占用空间往往小于对应类型的包装类型
  • 默认值:成员变量中包装类型不赋值就是null,而基本类型有默认值而不是null
  • 比较方式:对于基本数据类型用==,包装类型用equals方法

包装类型的缓存机制

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Integer缓存源码

public static Integer valueOf(int i) {

if (i >= IntegerCache.low && i <= IntegerCache.high)

return IntegerCache.cache[i + (-IntegerCache.low)];

return new Integer(i);

}

private static class IntegerCache {

static final int low = -128;

static final int high;

static {

// high value may be configured by property

int h = 127;

}

}

Boolean 缓存源码:

1
2
3
4
5
public static Boolean valueOf(boolean b) {

return (b ? TRUE : FALSE);

}

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

所有整型包装类对象之间值的比较,全部使用 equals 方法比较

浮点数运算精度丢失

通过BigDecimal可以实现对浮点数的运算以及精度的解决,一般用于金钱交易计算的场景才会用到

超过long整型数据如何表示

使用BigInteger内部使用int[]数组来存储任意大小的整形数据,但是相较于基本数据类型的计算效率会较低

成员变量与局部变量区别

  • 语法:成员变量可以被private、public、static等修饰,局部变量不能被访问修饰符、static修饰,但是成员变量和局部变量都能被final修饰
  • 存储方式:
    • 成员变量如果被static修饰,那么这个成员变量属于类,如果没有static修饰这个成员变量就是属于实例,存储在堆内存中
    • 局部变量存储在栈内存中
  • 生存时间:
    • 成员变量没有被static修饰的情况下,随着对象的创建而存在;被static修饰的情况下,类被卸载时,静态变量被销毁,并释放内存空间。static变量的生命周期取决于类的生命周期。
    • 局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡
  • 默认值:成员变量一般自动以类型的默认值而赋值(final修饰的话必须被显式的赋值),局部变量不会自动赋值

静态变量的作用

一般经常使用的常量就会放在一个专用的constant类中,通过static类进行修饰,可以被类的所有实例共享,由于static修饰的变量只会被分配一次内存,即使创建多个对象也只会分配一次,可以很大程度上的节约内存空间。

通常情况下static会和final连用

静态方法能否调用非静态成员?

不能,因为静态成员在类加载的时候就已经分配好了内存,也就是说静态成员属于类,而非静态成员只有在创建实例对象的时候才会生成,所以在静态方法中调用还没有生成的非静态成员属于非法操作

重写和重载的区别

重写发生在子类对父类的某一行为进行重新编写

重载发生在同一类中,方法名相同,参数不同、返回类型不同

区别点 重载 重写
发生范围 同一个类 子类
返回类型 可以修改 void或基本数据类型不可修改,引用类型可修改成该引用类型的子类
异常 可以修改 子类抛出比父类声明异常类更小或相等
访问修饰符 可以修改 子类可访问修饰符权限比父类松(比如父类是public、子类只能是public、而不能是protected、private、default这些)
参数列表 必须修改(参数属性,顺序) 一定不能修改
发生阶段 编译期 运行期

注意:重写时,如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。

面向过程和面向对象

面向对象:将对象抽象出来、然后调用对象的内部方法解决问题

面向过程:把解决问题的过程拆分成一个个方法,然后通过一个个方法的执行解决问题

对象实体和对象引用关系

  • 一个对象引用可以指向0或1个对象
  • 一个对象可以有n个引用指向它

对象实体存在内存之中,对象引用存在栈内存中

接口和抽象类的异同点

共同点:

  • 都不可以被实例化
  • 都可以包含抽象方法
  • 都可以有默认实现方法

不同点:

  • 接口是对行为的约束,实现了某一个接口就代表拥有了这个行为;抽象类是代码复用、强调的是从属关系。
  • 接口的成员变量必须被public static final修饰,抽象类变量默认default且可在子类被重新定义和赋值
  • 一个类只能继承一个类、但是一个类可以实现多个接口

深拷贝和浅拷贝

d4cf78ef9da72e3efcd689ad4b4fa92

浅拷贝:会再堆上创建一个新的对象,不过原对象内部的属性有引用类型的话,浅拷贝会直接内部对象的引用地址,即:拷贝对象和原始对象的内部对象共用一个内部对象。

深拷贝:会完全复制整个对象,包括这个对象所含的内部对象,也会对这个内部对象重新复制并分配一个新的内存空间,此时拷贝对象的内部对象指向新的被拷贝的引用地址。即拷贝对象和原始对象的内部对象的地址不是同一个了、也就不共用了。

==和equals()方法区别

== 对于基本数据类型和引用类型效果不同:

  • 对于基本数据类型,比较的是值的大小
  • 对于引用数据类型,比较的是对象的内存地址是否相等

equals()主要是重写和未重写的两种情况

  • 对于重写了的对象,比较的是对象的值/属性是否相等。(String包装类就是被重写了,比较的时候就会比较String的值是否相等)
  • 没有重写的,比较的是对象的内存地址是否相等,即跟==意义相等

hashcode()的作用

确定对象在哈希表中的索引位置,确定两个对象是否相等的前提就是先判断两个对象的哈希码是否相等,如果不相等就会默认这两个对象是两个不同的对象,如果哈希码相等就会再通过equals()方法进行进一步的判断。

总结如下:

  • 如果两个对象的哈希值相等,那么这两个对象不一定相等。
  • 如果两个对象的哈希值相等且equals()方法返回也是true,那么这两个对象相等
  • 如果亮哥对象的哈希值不相等,那么这两个对象不相等。

String、StringBuilder、StringBuffer区别

  • 可变性:String不可变,由于finalprivate修饰,且String类没有暴露的可以修改方法,其他两个都继承自一个类,他们都有append()方法可以对字符串进行修改
  • 安全性:
    • String不可变,线程安全
    • StringBuilder 没有同步锁或者对方法加同步锁,可能会出现线程安全问题
    • StringBuffer通过对方法加了同步锁或者调用的方法加了同步锁,线程安全
  • 性能:
    • String 每次改变都会创建一个新的String对象,然后指针指向新的对象
    • StringBuilderStringBuffer都是对本身进行操作,而不是创建新的对象并改变对象的引用,但是StringBuilder相较于StringBuffer 只会提升10~15%的性能提升,但是会承担一定的多线程不安全的风险。
  • 单线程用StringBuilder
  • 多线程用StringBuffer
  • 只进行一次创建操作使用String

String为什么不可变

  • finalprivate修饰的数组,String类没有可以提供/暴露修改这个字符串的方法
  • String类被final修饰导致其不能被继承,进而避免了子类破坏String的不可变

1、final修饰的如果是基本数据类型的话,则值不可变。如果修饰的是引用类型则此引用不能指向新的引用(新的对象)

2、final修饰的数组Array只能表示此时这个引用类型的地址不可变,而不是说存在这个数组里面的值不可变,示例如下:

1
2
3
final int[] value = {1,2,3}
int []another = {4,5,6}
value = another;//此时意味着要value数组的地址指向another数组的地址,编译器会报错,但是如果是直接修改value数组里面的值,是完全可以的

修改被final修饰的value数组里面的

1
2
final int[] value = {1,2,3};
value[2] = 100;//此时数组value变成了{1,2,100}

使用+号连接字符串还是StringBuilder

使用+号实际上就是通过StringBuilderappend()方法实现的,拼接完成之后调用toString()方法得到一个String对象。

如果是多次进行+号拼接的话,实际上就是通过for循环,创建多个StringBuilder 循环添加,而不是对同一个StringBuilder进行复用。

对字符串常量池的理解

字符串常量池是JVM为了提升性能减少内存消耗针对String类开辟的一片区域,主要是为了防止字符串的重复创建导致的内存不必要的消耗问题。

String str = new String(“abc“);会创建几个字符串对象?

1或2个字符串对象

1、如果”abc”不在字符串常量池中,就在堆内存中创建两个字符串对象,其中一个会将其引用保存在字符串常量池中并返回

2、如果“abc”在字符串常量池中,就会直接返回引用,并在堆中创建一个字符串对象。

String.intern()方法的作用

1、如果字符串常量池中有这个字符串对象的引用,就会直接返回此引用

2、如果字符串中没有保存这个字符串对象的引用,就会在字符串常量池中创建一个指向该字符串对象的引用并返回

注意:new一个对象的时候,和String.intern()对象如果字符串常量池中没有对应的字符串对象引用的时候,此时两种情况下创建的字符串引用的位置不一样,

1、new的时候是在堆中创建一个字符串对象,然后将这个对象的引用保存在字符串常量池中

2、intern()方法是直接在字符串常量池中创建字符串对象的引用并返回

泛型

作用:使用泛型参数可以增强代码的可读性和稳定性。白话来说,就是编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型,比如ArrayList<Person> arr = new ArrayList<>(); 以前没有泛型的List返回类型是Object,需要手动转换类型,使用泛型之后编译器自动转换。

使用方式:

  • 泛型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Result<T> {
private T key;

public Result(T key){
this.key = key;
}

public T getKey(){
return key;
}
}

//初始化泛型类实例
Result<Integer> res = new Result<>(1000);
  • 泛型接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定义泛型接口
public interface Generator<T> {public T method();}

//实现接口但是不指定类型
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}


//实现接口指定类型
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
  • 泛型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
   public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
//不同类型的包装类型调用泛型方法

// 创建不同类型数组:Integer和 String
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );

项目实践:

自定义后端返回结果的时候Result<T>

序列化、反序列化

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

应用场景:

  • 将对象存储到redis之类的数据库的时候需要将数据进行序列化、将对象读取出来的时候需要进行反序列化
  • 将对象写入内存的时候需要将数据进行序列化、将数据读取出来的时候需要进行反序列化。

序列化的主要目的就是通过网络传输数据或者将数据存储到文件系统,数据库,内存等中,将数据转换成二进制字节流或其他可取用的数据格式。

有些字段不想被序列化的时候怎么解决?

可以用transient关键字修饰,阻止被transient序列化的实例对象被序列化,如果某个对象先前已经被序列化,然后被transient修饰,再反序列化之后此变量值不会被持久化和恢复,而是被置成类型的默认值,如果是int那么反序列化之后就是0。

  • transient修饰的变量,被反序列化的情况下不会被持久化和恢复,而是被默认置成此类型的默认值
  • transient只能修饰变量,而不能修饰类或方法
  • transient对于static修饰的变量无用,因为transient修饰的变量是对象,但是static修饰的变量不属于任何变量,所以没用

为什么不使用jdk自带的序列化工具?

  • 不支持跨语言的调用
  • 性能差
  • 存在安全问题,输入的反序列化数据可被用户控制,攻击者可以通过构造恶意输入。