Java基础
Java中的基本类型
Java中有8种基本数据类型,分别为:
- 6种数字类型:
- 4种整数类型:byte、short、int、long
- 2种浮点类型:float、double
- 1种字符类型:char
- 1种布尔类型: boolean
基本类型 | 位数 | 字节 | 取值范围 | 默认值 |
---|---|---|---|---|
byte | 8 | 1 | -128~127 | 0 |
short | 16 | 2 | -32768(-2^15) ~ 32767(2^15 - 1) | 0 |
int | 32 | 4 | -2147483648 ~ 2147483647 | 0 |
long | 64 | 8 | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) | 0 |
char | 16 | 2 | 0 ~ 65535(2^16 - 1) | ‘u0000’ |
float | 32 | 4 | 1.4E-45 ~ 3.4028235E38 | 0 |
double | 64 | 8 | 4.9E-324 ~ 1.7976931348623157E308 | 0 |
boolean | 1 | true、false | false |
这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean
Java中基本类型和包装类型的区别
- 用途方面:除了定义一些常量和局部变量之外,我们在其他我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以
- 存储方式:基本数据类型的局部变量存放在Java虚拟机栈中的局部变量表中,基本数据类型的成员变量存放在Java虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中
- 占用空间:相比包装类型,基本数据类型占用的空间比较小
- 默认值:成员变量包装类型不赋值则默认值为null,而基本数据类型有默认值且不是null
- 比较方式:对于基本数据类型来说,==比较的是值。对于包装数据类型来说,==比较的是对象的内存地址。所有整型包装类对象之间的值的比较,全部都使用equals()方法
PS:数据类型存放在栈中是一个常见的误区! 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中
包装类型的缓存机制了解吗?
Java基本数据类型的包装类型大部分都用到了缓存机制来提升性能
- Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据
- Character 创建了数值在 [0,127] 范围的缓存数据
- Boolean 直接返回 True or False
- 两种浮点数类型的包装类 Float,Double 并没有实现缓存机制
1
2
3
4
5
6
7
8
9
10
11
12Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
PS: 所有整型包装类对象之间值的比较,全部使用 equals 方法比较
Java中自动装箱和拆箱?
- 装箱:将基本数据类型用它们对应的引用类型包装起来
- 拆箱:将包装类型转换为基本数据类型PS: 如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作
1
2Integer i = 10; //装箱:Integer i = 10 等价于 Integer i = Integer.valueOf(10)
int n = i; //拆箱:int n = i 等价于 int n = i.intValue()
成员变量与局部变量的区别
- 语法形式方面:从语法形式上看,成员变量属于类,而局部变量是在代码块或者方法中定义的变量或者是方法的参数‘;成员变量可以被public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰
- 存储方式:从变量在内存中的存储方式看,如果成员变量使用static修饰,则这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的;而对象存于堆内存,局部变量则存在栈内存
- 生存时间:从变量在内存中生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随方法的调用结束而消亡
- 默认值: 从变量是否有默认值来看,成员变量如果在初始化时赋值,则会自动以类型的默认值而赋值一种情况例外:被 final 修饰的成员变量也必须显式地赋值);而局部变量则不会自动被赋值
静态变量有什么作用
静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存
静态变量是通过类名来访问的,例如StaticVariableExample.staticVar(如果被 private关键字修饰就无法这样访问了)
字符型常量和字符串常量的区别
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符
- 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
- 占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节
静态方法为什么不能调用非静态成员?
需要结合JVM相关知识,主要原因如下:
- 静态方法属于类的,在类加载的时候就会分配内存,可以通过类名直接访问;而非静态成员属于实例对象,只有在对象实例化后才会存在,需要通过类的实例对象去访问
- 在类的非静态成员变量不存在的时候静态方法已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作
静态方法和实例方法有何不同?
- 调用方式:在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象
- 访问类成员是否存在限制:静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制
重载和重写的区别
- 重载:发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同
- 重写: 重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写
综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
面向对象
对象实例与对象引用区别
- 对象实例又new进行创建,对象实例存放在堆内存中
- 对象引用指向对象实例,对象引用存放在栈内存中
- 一个对象引用可以指向0个或者1个对象
- 一个对象实例可以有n个对象引用指向它
对象相等与引用相等的区别
- 对象的相等一般比较的是内存中存放的内存是否相等
- 引用相等一般是比较的是他们指向的内存地址是否相等
区分一下==和equals的在对象中使用区别 - ==运算符比较的是对象的引用是否相等
- equals比较的是对象内容是否相等,比如:比较两个字符串的内容,即使两个字符串的对象引用不同,只要内容相等
1 | String str1 = "hello"; |
类的构造方法可以不声明吗?
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作
如果个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了
构造方法特点如下:
- 名字与类名相同
- 没有返回值,单不能用void声明构造函数
- 生成类的对象是自动执行,无需调用
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况
面向对象三大特性
封装
封装是指把一个对象的属性隐藏在对象内部,不允许外部对象直接访问对象的内部信息,但是可以提供一些可以被外界访问的方法来操作属性。
继承
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法,在子类中是无法访问的
- 子类可以拥有自己的属性和方法,即子类可对父类进行扩展
- 子类可以用自己的方式实现父类方法
多态
多态表示一个对象具有多种状态,具体表现为使用父类的引用指向子类的实例
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系
- 引用类型变量发出的方法调用的是哪一个类中的方法,必须要在程序运行期间才能确定
- 多态不能调用“只在子类中存在但在父类中不存在”的方法
- 如果子类重写了父类的方法,真正执行的是子类重写的方法,如果子类没有重写父类的方法,执行的是父类的方法
接口和抽象类的共同点和区别?
共同点:
- 1.都不能被实例化
- 2.都可以包含抽象方法
- 3.都可以有默认的实现方法(Java 8可以使用default关键字在接口中定义默认的方法)
区别:
- 1.接口主要用于对类的行为进行约束,实现了某个接口就具有对应的行为;抽象类主要用于代码复用,强调的是所属关系
- 2.一个类只能继承一个抽象类,但可以实现多个接口
- 3.接口中的变量只能是public static final 类型的,不能被修改且必须有初始值;而抽象类的成员变量默认default,可以在子类中被重新定义,也可以被重新赋值
深浅拷贝的区别?什么是引用拷贝
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象的原对象共用一个内部对象
浅拷贝示例代码如下: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
28
29
30
31
32public class Address implements Cloneable{
private String name;
// 省略构造函数、Getter&Setter方法
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address;
// 省略构造函数、Getter&Setter方法
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());深拷贝:深拷贝会完全复制整个对象,包括这个对象锁包含的内部对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());引用拷贝:两个不同的引用指向同一个对象
Object类常见的方法有哪些?
Object类是一个特殊的类,是所有类的父类,主要提供一下11个方法:
1 | //native方法,用于返回当前运行时对象的class对象,使用了final关键字进行修饰,故不允许类重写 |
==与equals()的区别
==对于基本类型和引用类型的作用效果是不同的
- 对基本数据类型来说:==比较是值
- 对引用数据类型来说:==比较的是对象的内存地址
因为Java只有值传递,所以,对==来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址
equals()不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或者间接父类,因此所有的类都有equals()方法;equals方法存在两种使用情况:
- 类没有重写equals方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法
- 类重写了equals方法:一般我们重写equals方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回true
PS:String中的equals方法在内部是被重写过的,因为Object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象地址;当创建String类型的对象时,虚拟机会在常量池中查找到有没有已经存在的值和要创建的值相同的对象,如果有就将其赋值给引用;如果没有就在常量池中创建一个String对象
hashCode()有什么作用?
hashCode()的作用是获取哈希码,也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置;hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。
以“HashSet”中如何检查重复为例来说明为什么要有hashCode
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置
同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现
但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同
如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置
这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度
其实, hashCode() 和 equals()都是用于比较两个对象是否相等
那为什么JDK要同时提供者两个方法?
这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)
总结下来就是:
- 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)
- 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等
- 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等
为什么重写equals()时必须要重写hashCode()?
因为两个相等的对象的 hashCode 值必须是相等,也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等
如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等
总结
- equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等
- 两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)
String
String、StringBuffer、StringBuilder的区别
可变性
- String 是不可变的
- StringBuilder与StringBuffer都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法
线程安全性
- String 中的对象是不可变的,也就可以理解为常量,线程安全
- StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全
- StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的
性能
- 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象
- StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用
- 相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险
总结
- 操作少量的数据:使用String
- 单线程操作字符串缓冲去下大量数据: 使用StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
String为什么是不可变的?
首先,String中使用final关键字修饰字符数组来保存字符串,使用final关键字修饰有以下特点:
- final关键字修饰的类不能被继承
- final关键字修饰的方法不能被重写
- final关键字修饰基本数据类型的变量不能被改变
- final关键字修饰的引用类型变量不能再指向其他对象
String不可变的原因如下: - 保存字符串的数组被final修饰且为私有,并且String类没有提供修改这个字符串的方法
- String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变
字符串拼接使用+和StringBuilder的区别
- 字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现,拼接完成之后调用 toString() 得到一个 String 对象
- 在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象;如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题
String中的equals()和Object中equals()区别
String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等
Object 的 equals 方法是比较的对象的内存地址
字符串常量池的作用
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建
String中的intern()的作用
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回