Java学习笔记
Java学习笔记是一个持续更新的系列,工作多年,抽个空对自身知识做一个梳理和总结归纳,温故而知新,同时也希望能帮助到更多正在学习Java 的同学们。
本系列目录
入门篇
基础篇
数据类型
我们都知道Java语言是强类型语言,它的安全和健壮性部分也来自于此, 强类型语言即强制数据类型定义的语言。
一旦一个变量被指定了某个数据类型,如果不经过转换,那么该变量就永远是此数据类型了
在上一章中我们知道了变量需要申请内存空间来进行数据存储,那么给变量具体分配多少内存空间,就由变量的数据类型来决定,而在Java语言中有两大数据类型。
- 基本(内置)数据类型
- 引用数据类型
基本数据类型
基本数据类型,是Java内置的数据类型,直接存储在内存中的内存栈中,数据本身的值就是存储在栈空间里面,共有八种,又分为两大类,即数值类型,非数值类型,其中数值类型有六种,非数值类型有两种。
类型 | 关键字 | 大小 | 默认值 | 最大值 | 最小值 | 适用场景 |
---|---|---|---|---|---|---|
数值类型 | long | 64 位(8byte) | 0L | 9,223,372,036,854,775,807(2^63 -1) | -9,223,372,036,854,775,808(-2^63) | 整型,适用于比较大整数的系统上 |
数值类型 | double | 64 位(8byte) | 0.0d | +1.7 * 10^308 | -1.7 * 10^308 | 浮点型,适用于双精度的小数计算上 |
数值类型 | int | 32位(4byte) | 0 | 2,147,483,647(2^31 - 1) | -2,147,483,648(-2^31) | 整型,适用于整数计算 |
数值类型 | float | 32 位(4byte) | 0.0f | +3.4 * 10^38 | -3.4 * 10^38 | 浮点型,适用于单精度的小数计算上,比double占用内存少 |
数值类型 | short | 16 位(2byte) | 0 | 32767(2^15 - 1) | -32768(-2^15) | 整型,像 byte 那样节省空间 |
数值类型 | byte | 8位(1byte) | 0 | 127(2^7-1) | -128(-2^7) | 整型,适用于大型数组中节约空间,主要代替整数 |
非数值类型 | char | 16 位(2byte) | \u0000(空字符) | \uffff(即为 65535) | \u0000(即为 0) | 字符型,可以储存任何字符 |
非数值类型 | boolean | 没有明确大小 | false | 布尔型,只有两个取值,true和false |
我们也可以通过代码来查看基本数据类型的取值范围,大小和默认值,这里示范一种byte类型
public class HelloWrod {
byte by;
public static void main(String[] args) {
// byte
System.out.println("基本类型:byte 二进制位数:" + Byte.SIZE);
System.out.println("包装类:java.lang.Byte");
System.out.println("最小值:Byte.MIN_VALUE=" + Byte.MIN_VALUE);
System.out.println("最大值:Byte.MAX_VALUE=" + Byte.MAX_VALUE);
System.out.println("默认值 :" + by);
}
}
运行结果如下:
基本类型:byte 二进制位数:8
包装类:java.lang.Byte
最小值:Byte.MIN_VALUE=-128
最大值:Byte.MAX_VALUE=127
默认值 :0
类型转换
每个函数都可以强制将一个表达式转换成某种特定数据类型,在Java中,类型转换主要用在赋值和方法调用以及算术运算三种场景。
数据类型的转换是在所赋值的数值类型和被变量接收的数据类型不一致时发生的,它需要从一种数据类型转换成另一种数据类型。
基础数据类型的转换可以分为自动类型转换(隐式转换)和强制类型转换(显式转换)两种。
自动类型转换
当两种数据类型彼此兼容即都是基础数据类型,并且满足转换前的数据类型的位数要低于转换后的数据类型。
基础数据类型的转换规则
- 数值型数据的转换
byte→short→int→long→float→double - 字符型转换为整型
char→int
例如
byte 类型向 short 类型转换时,由于 short 类型的取值范围较大,会自动将 byte 转换为 short 类型,short类型转换byte时,则会出现编译错误。
在运算过程中,由于不同的数据类型会转换成同一种数据类型,所以整型、浮点型以及字符型都可以参与混合运算
强制类型转换
当两种数据类型不兼容,或目标类型的取值范围小于源类型时,自动转换将无法进行,这时就需要进行强制类型转换。
由于是大范围取值转为小范围取值,自然会丢失部分数据。
强制类型转换规则:(type)变量标识符
,type
是变量要转换成的数据类型。
目标取值范围小于源类型时,例如
short类型的强制转换为byte类型。
包装器类型,装箱与拆箱
Java为每种基本数据类型都提供了对应的包装器类型,而装箱就是 自动将基本数据类型转换为包装器类型,拆箱就是 自动将包装器类型转换为基本数据类型
Integer i = 10; //装箱
int n = i; //拆箱
int n2= i.intValue();//拆箱
Integer i2 =Integer.valueOf(n2); //装箱
装箱过程是通过调用包装器的valueOf
方法实现的,而拆箱过程是通过调用包装器的typeValue
方法实现的,type
指具体类型。
下表是基本数据类型对应的包装器类型:
基本数据类型 | 包装器类型 |
---|---|
int | Integer |
byte | Byte |
short | Short |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
引申一下,请问以下代码运行之后输出结果是什么?
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
正确的结果
true
false
你答对了吗?为什么会出现这种情况,其实是因为在通过valueOf
方法创建Integer
对象的时候,如果数值在[-128,127]
之间,便返回指向IntegerCache.cache
中已经存在的对象的引用,否则创建一个新的Integer
对象(具体内容需要跟踪Integer源代码)。
所有的包装器类型都属于引用数据类型,什么是引用数据类型呢?
引用数据类型
在Java中,引用类型指向一个对象,指向对象的变量是引用变量,所有引用类型的默认值都是null
,引用类型的数据存储在内存的堆中,一个引用变量可以用来引用任何与之兼容的类型。
引用数据类型是指由类型的实际值引用(类似于指针)表示的数据类型,如果为某个变量分配一个引用类型,则该变量将引用(或“指向”)原始值,“引用”(reference)是c++的一种新的变量类型,是对C的一个重要补充。
引用类型继承于Object
类(也是引用类型)都是按照Java里面存储对象的内存模型来进行数据存储的,使用Java内存堆和内存栈来进行这种类型的数据存储,即“引用”是存储在有序的内存栈上的,而对象本身的值存储在内存堆上的;
Java中提供了四种引用类型,由于优化JVM垃圾回收机制,也便于让程序员通过代码决定某些对象的生命周期,Java程序中默认95%情况下都是强引用类型。
-
强引用
是指创建一个对象并把这个对象赋给一个引用变量,强引用是我们最常见的普通对象引用。对于强引用对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
只要还有强引用指向一个对象,就能表明对象还活着,垃圾收集器不会碰这种对象。
当一个对象被强引用变量引用时,它处于可达状态,不可能被垃圾回收,即该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成java内存泄漏的主要原因之一。例如
User user=new User(); String str ="hello";
-
软引用
如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它。需要用
java.lang.ref.SoftReference
类来实现,相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集。
通常用在对内存敏感的程序中,比如缓存就有用到软引用。例如
import java.lang.ref.SoftReference; public class HelloGC { /** * 内存足够就保留,不够用就回收 */ public static void softRef_memory_enough(){ Object object1=new Object(); SoftReference<Object> softReference=new SoftReference<>(object1); System.out.println(object1); System.out.println(softReference.get()); object1=null; System.gc(); System.out.println(object1); System.out.println(softReference.get()); } public static void softRef_memory_notEnough(){ Object object1=new Object(); SoftReference<Object> softReference=new SoftReference<>(object1); System.out.println(object1); System.out.println(softReference.get()); object1=null; try{ byte[] bytes=new byte[30*1024*1024];//故意设置大内存,大于jvm分配内存,触发垃圾回收机制 }catch (Throwable e){ e.printStackTrace(); }finally { System.out.println(object1); System.out.println(softReference.get()); } } public static void main(String[] args) { HelloGC.softRef_memory_notEnough(); } }
-
弱引用
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在Java中,用
java.lang.ref.WeakReference
类来表示。
通常适用于加载本地资源时,由于静态资源从硬盘读取速度较慢,加载进内存又占用内存空间,使用弱引用可以解决该问题。例如
public static void main(String[] args) { Object o = new Object(); WeakReference<Object> weakReference=new WeakReference<>(o); System.out.println(o); System.out.println(weakReference.get()); o=null; System.gc(); System.out.println(o); System.out.println(weakReference.get()); }
-
虚引用
如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。
在Java中用java.lang.ref.PhantomReference
类表示。
虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。例如
public static void main(String[] args) throws InterruptedException { Object o1=new Object(); ReferenceQueue<Object> referenceQueue=new ReferenceQueue<>(); PhantomReference<Object> phantomReference=new PhantomReference(o1,referenceQueue); System.out.println(o1); System.out.println(phantomReference.get());//虚引用,形同虚设,所以get都是null System.out.println(referenceQueue.poll()); System.out.println("-----------------------"); o1=null; System.gc(); Thread.sleep(500); System.out.println(o1); System.out.println(phantomReference.get()); System.out.println(referenceQueue.poll()); }
引用类型间的类型转换
引用类型间的转换有自动类型转换和强制类型转换两种。
对于引用类型,只有具有继承关系的类和接口,即父类与子类之间,实现类与接口,才可以进行类型的转换。
自动类型转换
子类的变量对其父类的变量赋值时,会自动进行类型的转换,接口的实现类赋值给接口时,也会自动进行类型转换。
public class HelloWrod extends Object{
public static void main(String[] args) {
HelloWrod helloWrod=new HelloWrod();
Object obj=helloWrod;//自动转换。
HelloWrod helloWrod2=obj;//会报错
HashMap hashMap=new HashMap<>();//HashMap实现了Map接口
Map map=hashMap;//自动转换
}
}
如果非要使用父类对子类赋值,则需要进行强制转换。
强制类型转换
父类对子类进行赋值时则需要进行强制类型转换。
强制转换需要使用(Class)Type
的方式进行,Class
是强制转换的目标类名称,Type
是被转换的对象,强制类型转换会丢失部分信息。
public class HelloWrod extends Object{
public static void main(String[] args) {
Object obj=new Object();
HelloWrod helloWrod2=(HelloWrod) obj;//强制转换
}
}
但是强制类型转换不能用于俩个不同的子类或者没有任何关系的类之间
错误示范
class HelloWord3 {
}
class HelloWord2 extends Object{
}
public class HelloWrod extends Object{
public static void main(String[] args) {
HelloWord2 helloWord2=new HelloWord2();
HelloWord3 helloWord3=new HelloWord3();
HelloWrod helloWrod=helloWord2;//编译出错
HelloWrod helloWrod1=(HelloWord2)helloWord2;//编译出错
HelloWrod helloWrod2=helloWord3();//编译出错
HelloWrod helloWrod2=(HelloWord2)helloWord3();//编译出错
}
}
String类型
在Java中String是一个字符串类,使用频繁,它属于强引用类型,所有类似“”
中的值,都是String类的实例,其类和方法都被final
关键字修饰,不可继承,不可修改。
String类位于
java.lang
包下,提供了字符串的比较、查找、截取、大小写转换等操作。
Java语言为“+”
连接符(字符串连接符),字符串对象可以使用“+”连接其他对象。
通过“+”
连接符可以自动将基础类型转换为字符串类型。
String是有一定的特殊性,Java为String 类提供了两种声明方式,这两种方式对应的内存分配方案也有所不同。
在JVM内存区域中,堆区域分成了两块,一块是字符串常量池(String constant pool),用于存储Java字符串常量对象,另一块用于存储普通对象及字符串对象。
两种声明方式
-
直接赋值
声明变量时直接赋予值。public static void main(String[] args) { String value1="hello string"; String value2="hello string"; System.out.println(value1==value2); }
打开之前我们的
HelloWrod.java
文件,在main方法中创建字符串value1
和value2
,比较它们的地址值(==
进行的是地址值的比较),结果true
这段代码内存分配大概步骤如下
- JVM在栈区域开辟
main
方法栈 - 根据方法的内部变量顺序,在栈帧中首先开辟
value1
的地址空间,同时在堆中实例化字符编码数组,数组长度为value1
长度,内容为value1
的每个字符的编码值 - 通过直接赋值的方式声明String类型的对象时,首先会在堆中的字符串常量池中进行匹配,如果没找到,则在其内部实例化一个String对象,将上述字符编码数组(
value1
)的首地址赋值给String对象,相当于字符串常量池中的一个字符串对象指向一个字符编码数组 - 将常量池中的字符串的地址赋值给栈帧中的
value1
变量,相当于变量value1
指向/引用了该字符串对象 - 继续开辟
value2
的地址空间,同时在堆中字符串常量池中寻找(遍历字符串常量池中储存的字符编码数组),一旦发现与value2
值相同的字符串实例,则将其在堆内存中的地址值赋值给value2变量
- JVM在栈区域开辟
-
通过关键字
new
声明
声明变量时,通过new关键字调用构造函数完成。public static void main(String[] args) { String value1="hello string"; String value2="hello string"; String value3=new String("hello string");//通过关键字`new`声明 System.out.println(value1==value2);//输出true System.out.println(value1==value3);//输出false }
value3
被存放在栈区,同时在堆区开辟一块内存用于存放一个新的String类型对象。这段代码内存分配大概步骤如下
- 在栈帧中开辟
value3
的地址空间,利用字符数组实例化字符串对象时,JVM首先会将目标字符数组转化为字符编码数组 - 一切
new
出来的对象都在堆内存中。
JVM在堆内存中实例化一个String类对象,将上述字符编码数组的首地址赋值给字符串对象 - 将字符串对象的地址赋值给栈帧中的字符串类型变量
value3
- 在栈帧中开辟
字符串中的特殊值
Java中,在“”
中的都是字符串,这其中有一些特殊意义转义字符串存在。
根据 Java Language Specification 的要求,Java 源代码的字符串中的反斜线被解释为 Unicode 转义或其他字符转义。
转义字符 | 含义 | ASCII码(16/10进制) |
---|---|---|
\0 | 空字符(NULL) | 00H/0 |
\n | 换行符(LF) | 0AH/10 |
\r | 回车符(CR) | 0DH/13 |
\t | 水平制表符(HT) | 09H/9 |
\v | 垂直制表(VT) | 0B/11 |
\a | 响铃(BEL) | 07/7 |
\b | 退格符(BS) | 08H/8 |
\f | 换页符(FF) | 0CH/12 |
\’ | 单引号 | 27H/39 |
\” | 双引号 | 22H/34 |
\\ | 反斜杠 | 5CH/92 |
\? | 问号字符 | 3F/63 |
\ddd | 任意字符 | 三位八进制 |
\xhh | 任意字符 | 二位十六进制 |
System.out.println("hello \n word!");//输出换行结果
System.out.println("\r \n hello word!");//输出回车之后换行
ASCII码
目前计算机中用得最广泛的字符集及其编码,是由美国国家标准局(ANSI)制定的ASCII码(American Standard Code for Information Interchange,美国标准信息交换码),它已被国际标准化组织(ISO)定为国际标准,称为ISO 646标准。适用于所有拉丁文字字母,ASCII码有7位码和8位码两种形式。
运行该程序可以实现字母转换ASCII码
public static void main(String[] args) {
//在键盘上输入任意一个字母 ,显示ASCII码值
Scanner in = new Scanner(System.in);
String b = in.next();
for( int i=0;i< b.length();i++){
System.out.println( b.charAt(i)+" "+(byte) b.charAt(i));
}
}
Unicode编码
全球文字统一编码,Unicode把世界上的各种文字的每一个字符指定唯一编码,实现跨语种、跨平台的应用。
在JVM中中字符只以一种形式存在,那就是Unicode,不选择任何特定的编码,直接使用它们在字符集中的编号,这是统一的唯一的方法。
当着字符从JVM内部移动到外部时(即保存为文件系统中的一个文件内容时),就进行了编码转换,使用了具体的编码方案。
因此也可以说,所有的编码转换只发生在边界的地方,也就是各种输入/输出流的起作用的地方。
编译器把Java源文件编译成.class
文件的时候需要用到文件的编码,我们一般设置成UTF-8
,但是很多编译器默认的编码并不是UTF-8
,而是ISO
或者其他编码,如果不注意检查,可能导致我们的程序出现中文乱码。
intern方法
我们已经知道直接使用双引号声明出来的String对象会直接存储在字符串常量池中,如果不是用双引号声明的String对象,想要放入常量池中,可以使用String提供的intern方法。
intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。
示例
public static void main(String[] args) {
String value1=new String("java");
String value2="java";
System.out.println(value1==value2);
System.out.println(value1.intern()==value2);
System.out.println(value1.intern()==value1);
}
//运行之后返回结果如下
//false
//true
//false
字符串与基本类型的相互转换
基本类型转字符串可通过“+”
符号直接被转换成字符串类型,也可以通过String
类的valueOf()
方法进行转换,而包装类型也拥有将字符串转换为其基础类型的函数方法。
int n = 1;
Integer n2=1;
String s=n+"";//基础类型转换字符串
String s1=String.valueOf(n);//基础类型转换字符串
String s2=n2.toString();//包装类型转换字符串
int n3 = Integer.parseInt("1");//字符串转换为基础类型
包装类都属于引用类型,继承Object
对象的toString()
方法可以返回其数值的字符串值,而包装类型的parseXXX()
方法均可将字符串转换为其基础类型。
字符串与引用类型的相互转换
字符串无法直接转换成引用类型,但是引用类型自带的toString()
方法都会返回其引用对象地址,一般我们在工作中都会重写该方法,来返回业务相关的字符串内容。
字符串转引用对象,需要字符串遵守一定规则,例如json格式的字符串,通过反射转换成对应对象,具体实现细节后续细说。
枚举类型
枚举是Java中一个特殊的数据类型,限制变量要有一些预先定义的值,枚举列表中的值被称为枚举值,枚举属于引用类型。
枚举是在 Java5.0 版本中被引进的,既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性,因此运用枚举值可以大大减少你的代码中的漏洞。
枚举的定义与使用
使用关键字enum
定义枚举,枚举可以定义在类内部,也可以独立定义为一个枚举类文件。
如果我们要将一个物品的尺寸限制为小、中、大,这样就可以确保人们不会把需求定在指定范围之外。
//内部
class Goods {
enum GoodsSize{ SMALL, MEDIUM, LARGE }
GoodsSize size;
}
public class GoodsTest {
public static void main(String args[]){
Goods goods = new Goods ();
goods.size = Goods.GoodsSize.MEDIUM ;
System.out.println("Size: " + goods.size);
}
}
/******************分割线**************************/
//外部
enum GoodsSize{ SMALL, MEDIUM, LARGE }
class Goods {
GoodsSize size;
}
public class HelloWrod extends Object{
public static void main(String[] args) {
Goods goods = new Goods ();
goods.size = GoodsSize.MEDIUM ;
System.out.println("Size: " + goods.size);
}
}
输出如下结果:Size: MEDIUM
注:枚举可以自己声明也可以在类中声明,方法变量和构造器也可以在枚举值中定义。
枚举也是一个类,可以实现接口,但是不能继承其他类,因为所有的枚举都继承自java.lang.Enum
类。
public interface Behaviour {
void print();
String getInfo();
}
public enum Color implements Behaviour{
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
//接口方法
@Override
public String getInfo() {
return this.name;
}
//接口方法
@Override
public void print() {
System.out.println(this.index+":"+this.name);
}
}
Enum类
Enum是抽象类,所有 Java 语言枚举类型的公共基本类,它还提供了一些针对枚举的使用方法:
返回类型 | 方法名称 | 方法说明 |
---|---|---|
int | compareTo(E o) | 比较此枚举与指定对象的顺序 |
boolean | equals(Object other) | 当指定对象等于此枚举常量时,返回 true。 |
Class<?> | getDeclaringClass() | 返回与此枚举常量的枚举类型相对应的 Class 对象 |
String | name() | 返回此枚举常量的名称,在其枚举声明中对其进行声明 |
int | ordinal() | 返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为0) |
String | toString() | 返回枚举常量的名称,它包含在声明中 |
static<T extends Enum> T | static valueOf(Class enumType, String name) | 返回带指定名称的指定枚举类型的枚举常量。 |
进阶用法
枚举除了基本的用法之外,也可以当作一个Java版的静态数据库来使用,由于枚举除了无法继承之外也是一个类,具有定义方法和成员的功能,我可以通过构造方法,将枚举改造成一个静态数据库来用。
import java.util.concurrent.CountDownLatch;
/***
* 枚举应用,java版数据库
*/
enum UsersEnum{
a(1,"张三"),b(2,"李四"),c(3,"王五"),d(4,"赵六"),e(5,"刘其"),f(0,"宋八");
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//添加构造函数
UsersEnum(Integer id,String name){
this.id=id;
this.name=name;
}
//添加操作方法,根据id返回name
public static UsersEnum forEachEnum(int index){
UsersEnum[] myArray=UsersEnum.values();
for (UsersEnum element:myArray) {
if(index==element.id){
return element;
}
}
return null;
}
}
public class UsersEnumDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <6 ; i++) {
System.out.println(UsersEnum.forEachEnum(i).getName());
}
}
}
在上述示例中,通过添加构造函数,实现多行数据的添加。
枚举还支持抽象方法,接口等,根据实际业务需要,可以拓展出更多用法。