JavaSE-基础知识(一)

基本数据类型

什么是浮点型

由于有些带小数的二进制表示中可能会出现无限循环的情况,这种情况计算机就没有办法用二进制精准的表示。所以,为了解决部分小数无法使用二进制精确表示的问题,于是就有了IEEE754规范

浮点数和小数并不是完全一样的,计算机中小数的表示法,其实有定点和浮点两种。因为在位数相同的情况下,定点数的表示范围要比浮点数小。所以在计算机学科中,使用浮点数来表示实数的近似值。

IEEE并没有解决小数无法精确表示的问题,只是提出了一种近似值表示小数的方式,并且引入了精度的概念。

什么是单精度和双精度

单精度浮点数在计算机存储器中占用4个字节(32bits),利用浮点的方法,可以表示一个范围很大的数值。比起单精度浮点数,双精度浮点数(double)使用64位来存储一个浮点数。

为什么不能用浮点型表示金额

由于计算机中保存的小数其实是十进制的小数的近似值,并不是准确值,所以,不能在代码中用浮点数来表示金额等重要的指标。建议使用BigDecimal或者Long(单位为分)来表示金额。

自动拆装箱

Java是一种强类型语言,第一次申明变量必须说明数据类型。Java中的数值类型不存在无符号的,它们的取值范围是固定的,不会随着机器硬件或者操作系统的改变而改变。

除了八种基本类型外,Java还存在另一种基本类型void,它也有对应的包装类 java.lang.Void,不过我们无法直接对它们进行操作。

基本类型有什么好处

Java对象中,new一个对象是存储在堆里的,我们通过栈中的引用来使用这些对象;所以,对象本身来说是比较消耗资源的。

基本类型如int,若每次使用这种变量都要new一个Java对象的话,就会比较笨重。所以,Java提供了一种机制,使这种数据的变量不需要new创建,他们不会在堆上创建,而是直接在栈内存中存储,因此会更加高效。

包装类型

Java 语言是一个面向对象的语言,但是 Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。

包装类均位于 java.lang 包,包装类和基本数据类型的对应关系如下表所示

image-20211203151554629

在这八个类名中,除了 Integer 和 Character 类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可。

为什么需要包装类

因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int 、double 等类型放进去的。因为集合的容器要求元素是 Object 类型。

为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

拆箱与装箱

有了基本类型和包装类,肯定有些时候要在他们之间进行转换。我们认为包装类是对基本类型的包装,所以,把基本数据类型转换成包装类的过程就是boxing,译为装箱。反之,把包装类转换成基本数据类型的过程就是拆包装,即拆箱。

自动拆箱与自动装箱

为了减少开发人员的工作,Java提供了自动拆装箱功能。

自动装箱:就是将基本数据类型自动转换成对应的包装类

自动拆箱:就是将包装类自动转换成对应的基本数据类型

1
2
3
//可以替代 Integer i = new Integer(10);
Integer i = 10; //自动装箱
int b = 1; //自动拆箱

自动拆装箱的实现原理

int的自动装箱是通过Integer.valueOf()方法来实现的,Integer的自动拆箱都是通过integer.intValue来实现的。

自动装箱都是通过包装类的valueOf()方法来实现的,自动拆箱都是通过包装类对象的xxxValue()来实现的

那些地方会自动拆装箱

除了前面提到的变量的初始化和赋值的场景外,主要有以下场景

一、将基本数据类型放入集合类

1
2
3
4
5
6
7
8
9
10
11
12
   List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i ++){
//Java集合类只能接收对象类型,那么下面代码为和不报错
li.add(i);
}

//将上面代码进行反编译
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2){
//当我们把基本数据类型放入集合类中的时候,会进行自动装箱
li.add(Integer.valueOf(i));
}

img

Java编译是指将Java源码编译成Java字节码的过程

Java反编译是指将Java字节码“翻译”成源码的过程

二、包装类型和基本类型的大小比较

1
2
3
4
5
6
7
8
9
10
   Integer a = 1;
System.out.println(a == 1 ? "等于" : "不等于");
Boolean bool = false;
System.out.println(bool ? "真" : "假");

//反编译
Integer a = 1;
System.out.println(a.intValue() == 1 ? "等于" : "不等于");
Boolean bool = false;
System.out.println(bool.booleanValue ? "真" : "假");

其他场景还有包装类型的运算、函数参数和返回值等等

Integer的缓存机制

在Java5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。

适用于整数值区间-128至+127

只适用于自动装箱。使用构造函数创建对象不适用。

通过测试下面代码的输出,以明白引入的缓存行为的作用

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
package com.javapapers.java;

public class JavaIntegerCache {
public static void main(String... strings) {

Integer integer1 = 3;
Integer integer2 = 3;

if (integer1 == integer2)
System.out.println("integer1 == integer2");
else
System.out.println("integer1 != integer2");

Integer integer3 = 300;
Integer integer4 = 300;

if (integer3 == integer4)
System.out.println("integer3 == integer4");
else
System.out.println("integer3 != integer4");

}
}
//上面代码的输出是
integer1 == integer2
integer3 != integer4

String

字符串的不可变性

一旦一个string对象在内存(堆)中被创建出来,就无法修改。特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。

如果需要一个可修改的字符串,应该使用StringBuffer或者StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string对象被创建出来。

replaceFirst、replaceALL、replace区别

replace(CharSequence target, CharSequence replacement) ,用replacement替换所有的target,两个参数都是字符串。

replaceAll(String regex, String replacement) ,用replacement替换所有的regex匹配项,regex很明显是个正则表达式,replacement是字符串。

replaceFirst(String regex, String replacement) ,基本和replaceAll相同,区别是只替换第一个匹配项。

可以看到,其中replaceAll以及replaceFirst是和正则表达式有关的,而replace和正则表达式无关。

replaceAllreplaceFirst的区别主要是替换的内容不同,replaceAll是替换所有匹配的字符,而replaceFirst()仅替换第一次出现的字符

String对”+”的重载

  1. String s = "a" + "b",编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),即变成 String s = "ab"
  2. 对于能够进行优化的(String s = “a” + 变量 等)用 StringBuilderappend()方法替代,最后调用toString()方法 (底层就是一个 new String())

字符串拼接的几种方式和区别

String是Java中一个不可变的类,一旦被实例化就无法修改。这样设计的好处有很多,比如可以缓存hashcode,使用更加便利以及更加安全等。

所谓的字符串拼接,都是重新生成了一个新的字符串

1
2
String s = "abcd";
s = s.concat("ef");

img

s中保存的是重新创建出来的String对象的引用。

使用+拼接字符串

1
2
3
4
5
6
String wechat = "zhaoxfan";
String introduce = "Java";
String hollis = wechat + "," + introduce;
/*
+不是运算符重载,并且Java不支持运算符重载。这只是Java提供的一个语法糖
*/

运算符重载:多态的一种,就是对已有的运算符重新定义,赋予其另一种功能,以适应不同的数据类型。

语法糖:指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

concat

1
2
3
String wechat = "zhaoxfan";
String introduce = "Java";
String hollis = wechat.concat(",").concat(introduce);

StringBuffer

1
2
3
StringBuffer wechat = new StringBuffer("zhaoxfan");		//该对象可扩充和修改
String introduce = "Java";
StringBuffer sb = wechat.append(",").append(introduce);

**StringBuilder**

1
2
3
StringBuilder wechat = new StringBuilder("zhaoxfan");
String introduce = "Java";
StringBuilder sb = wechat.append(",").append(introduce);

StringUtils.join

1
2
3
4
String wechat = "zhaoxfan";
String introduce = "Java";
//join方法最主要的功能是将数组或集合以某种拼接符拼接到一起形成新的字符串
System.out.println(StringUtils.join(wechat, ",", introduce));

关于上述实现方法的优劣以及原理可参考

从结果看,用时从短到长的对比是:

StringBuilder < StringBuffer < concat < + < StringUtils.join

StringBuilder天生就是设计来定义可变字符串和字符串的变化操作的。但是值得注意的是,如果不是在循环体中进行字符串拼接的话,直接使用+就好了;如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder。因为StringBuffer是并发安全类型。

String.valueOf和Integer.toString的区别

1
2
3
4
int i = 5;
String i1 = "" + i; //String i1 = (new StringBuilder()).append(i).toString();
String i2 = String.valueOf(i); //实际上也是调用Integer.toString(i)来实现的
String i3 = Integer.toString(i);

switch对String的支持

switch对char类型进行比较的时候,实际上比较的是ascill码,编译器会把char型变量转换成对应的int型变量

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class switchDemoString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default:
break;
}
}
}

//反编译
public class switchDemoString
{
public switchDemoString()
{
}
public static void main(String args[])
{
String str = "world";
String s;
switch((s = str).hashCode())
{
default:
break;
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
}
}
}

/*
通过反编译,我们知道原来字符串的switch是通过equals()和hashCode()方法来实现的
switch中只能使用整型,如byte、short、char(ascill是整型)以及int
其他数据类型都是转换成整型之后再使用switch的
*/

字符串池

在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存叫做字符串常量池。

当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。

这种机制,就是字符串驻留或池化。

除了以上方式之外,还有一种可以在运行期将字符串内容放置到字符串常量池的方法,那就是使用intern。

intern在每次赋值的时候使用String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用。

Java关键字

transient

变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。这里的对象存储是指,Java的serialization提供的一种持久化对象实例的机制。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。使用情况是:当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的域上关闭serialization,可以在这个域加上关键字transient。

简单点说,就是被transient修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。

instanceof

二元操作符,作用是测试它左边的对象是否是它右边的类的实例,返回boolean的数据类型。

1
2
3
4
5
6
7
8
public static void displayObjectClass(Object o) {
if (o instanceof Vector)
System.out.println("对象是 java.util.Vector 类的实例");
else if (o instanceof ArrayList)
System.out.println("对象是 java.util.ArrayList 类的实例");
else
System.out.println("对象是 " + o.getClass() + " 类的实例");
}

volatile

volatile在Java中是提供可见性和有序保障等的,是Java并发编程中比较重要的一个关键字。volatile是一个变量修饰符,只能用来修饰变量,无法修饰方法及代码块等。

volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

代码是一个比较典型的使用双重锁校验的形式实现单例的,其中使用volatile关键字修饰可能被多个线程同时访问到的singleton

等学了并发编程再来

synchronized

等学了并发编程再来

final

final所表示的是“这部分是无法修改的”,使用final可以定义:变量、方法、类

变量设置为final,则不能更改它的值;方法声明为final,则不能覆盖它;类声明为final,则不能继承它。

static

static表示“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块

静态变量

static可表示变量的级别,一个类中的静态变量,不属于类的对象或者实例。因为静态变量与所有的对象实例共享,因此他们不具线程安全性。

通常,静态变量常用final关键字来修饰,表示通用资源或可以被所有的对象所使用。如果静态变量未被私有化,可以用“类名.变量名”的方式来使用。

静态方法

与静态变量一样,静态方法是属于类而不是实例。一个静态方法只能使用静态变量和调用静态方法。通常静态方法通常用于想给其他的类使用而不需要创建实例。例如:Collections class(类集合)。

Java的包装类和实用类包含许多静态方法。main()方法就是Java程序入口点,是静态方法。

静态代码块

Java的静态块是一组指令在类装载的时候在内存中由Java ClassLoader执行。

静态块常用语初始化类的静态变量。大多数还用于在类装载时候创建静态资源。

Java不允许在静态块中使用非静态变量。一个类中可以有多个静态块,静态块只在类装载入内存时,执行一次

const

Java的预留关键字,用于后期扩展用,用法跟final相似,不常用

集合类

Collection和Collections区别

Collection是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java类库中有很多具体的实现。是list,set等的父接口。

Collections是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

日常开发中,了解Java中的Collection及其子类的用法以及Collections用法,可以提升很多处理集合的效率。

常用集合类的使用

Set和List区别

List和Set都是继承自Collection接口,都是用来存储一组相同类型的元素的。

List特点:元素有放入顺序(先放入的元素排在前面),元素可重复。

Set特点:元素无放入顺序,元素不可重复。

ArrayList和LinkedList和Vector的区别

List主要有ArrayListLinkedList与Vector几种实现。

这三者都实现了List接口,使用方式也很相似,主要区别在于因为实现方式的不同,所以对不同的操作具有不同的效率、

ArrayList是一个可变大小的数组,当更多元素加入到ArrayList中时,其大小将会动态地增大,内部的元素可以直接通过get与set方法进行访问,因为ArrayList本质上就是一个数组。

ArrayList实现了writeObject方法,只保存了非null的数组位置上的数据。即list个数的elementData

LinkedList是一个双链表,在添加和删除元素时具有比ArrayList更好的性能。但在get与set方面弱于ArrayList。(这些对比都是指数据量很大或者操作很频繁的情况下的对比)

Vector 和ArrayList类似,但属于强同步类。如果你的程序本身是线程安全的(thread-safe,没有在多个线程之间共享同一个集合/对象),那么使用ArrayList是更好的选择。Vector也实现了writeObject方法,但方法并没有像ArrayList一样进行优化存储,实现语句是

1
data = elementData.clone();

clone()的时候也会把null值也拷贝,所以保存相同内容的Vector与ArrayList,Vector的占用字节比ArrayList更多。

ArrayList是非同步实现的一个单线程下较为高效的数据结构(相比Vector来说)。其存储结构定义为transient,重写writeObject来实现自定义的序列化,优化了存储。

Vector是多线程环境下更为可靠的数据结构,所有方法都实现了同步。

Vector和ArrayList在更多元素添加进来时会请求更大的空间。Vector每次请求其大小的双倍空间,而ArrayList每次对size增长50%.

LinkedList还实现了Queue接口,该接口比List提供了更多的方法,包括 offer(),peek(),poll()等.

注意: 默认情况下ArrayList的初始容量非常小,所以如果可以预估数据量的话,分配一个较大的初始值属于最佳实践,这样可以减少调整大小的开销。

Set如何保证元素不重复

在Java的Set体系中,根据实现方式不同主要分为两大类。HashSetTreeSet

1、TreeSet是二叉树实现的,TreeSet中的数据是自动排序好的,不允许放入null值

2、HashSet是哈希表实现的,HashSet中的数据是无序的,可以放入null值,但只能放入一个null。两者中的值都不能重复,就如数据库中的唯一约束。

HashSet中,基本的操作都是由HashMap底层实现的,因为HashSet底层是用HashMap存储数据的。当向HashSet中添加元素的时候,首先计算元素的hashCode值,然后通过扰动计算和按位与的方式计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。

TreeSet的底层是TreeMapkeySet(),而TreeMap是基于红黑树实现的,红黑树是一种平衡二叉查找树,它能保证任何一个节点的左右子树的高度差不会超过较矮的那棵的一倍。

TreeMap是按key排序的,元素在插入TreeSetcompareTo()方法要被调用,所以TreeSet中的元素要实现Comparable接口。TreeSet作为一种Set,它不允许出现重复元素。TreeSet是用compareTo()来判断重复元素的。

HashMap、HashTable、ConcurrentHashMap区别

线程安全:HashTable 中的方法是同步的,而HashMap中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用HashTable,但是要使用HashMap的话就要自己增加同步处理了。

继承关系:HashTable是基于陈旧的Dictionary类继承来的。 HashMap继承的抽象类AbstractMap实现了Map接口。

允不允许null值: HashTable中,key和value都不允许出现null值,否则会抛出NullPointerException异常。 HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。

默认初始容量和扩容机制: HashTable中的hash数组初始大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

哈希值的使用不同 :HashTable直接使用对象的hashCodeHashMap重新计算hash值。

ConcurrentHashMapHashMap的实现方式不一样,虽然都是使用桶数组实现的,但是还是有区别,ConcurrentHashMap对桶数组进行了分段,而HashMap并没有。ConcurrentHashMap在每一个分段上都用锁进行了保护。HashMap没有锁机制。所以,前者线程安全的,后者不是线程安全的。

Arrays.asList

asList得到的只是一个Arrays的内部类,一个原来数组的视图List,因此如果对它进行增删操作会报错;用ArrayList的构造器可以将其转变成真正的ArrayList

Collection如何迭代

Collection的迭代有很多方式:

  1. 通过普通for循环迭代
  2. 通过增强for循环迭代
  3. 通过Iterator迭代
  4. 使用Stream迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
List<String> list = ImmutableList.of("zhao", "xfan");

// 普通for循环遍历
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}

//增强for循环遍历
for (String s : list) {
System.out.println(s);
}

//Iterator遍历
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}

//Stream 遍历
list.forEach(System.out::println);

list.stream().forEach(System.out::println);