本文介绍Java中常用的几个类。
1. 数组
1.1 数组简介
Java中的数组属于引用数据类型,不是基本数据类型,它的父类是Object。因为其是引用数据类型,所以数组对象存储在堆内存中。另外数组中如果存储的是引用数据类型数据的话,实际上存储的是其引用(内存地址)。数组一旦创建,长度不可变。
数组分为一维数组、二维数组、三维数组、多维数组…。所有的数组对象都有length属性,用来获取数组中元素的个数。Java中的数组要求数组中元素的类型统一,比如int类型数组只能存储int类型。数组中首元素的内存地址作为整个数组对象的内存地址。可以把int[]
整体看做是一个具体的引用数据类型。
1.2 数组语法
和基本数据类型一样,数组作为引用数据类型也需要声明和初始化。
声明数组的语法格式如下:
1 | 数据类型[] 数组名; |
初始化数组的语法格式如下:
1 | // 静态初始化一维数组 |
静态初始化例子如下:
1 | public class ArrayTest01 { |
动态初始化例子如下:
1 | public class ArrayTest02 { |
1.3 数组扩容
在Java语言中,数组长度一旦确定不可变,那么数组满了需要扩容,该怎么做呢?先创建一个大容量数组,然后将小容量数组中的数据一个个拷贝到大数组中。数组拷贝方法system.arraycopy()
。
1 | public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) |
将src数组中srcPos位置开始,拷贝长度为length,将其拷贝到数组dest中,起始位置是destPos。
所以数组扩容效率较低,因为涉及到拷贝的问题。因此在以后的开发中要注意:尽可能少的进行数组拷贝,可以在创建数组对象的时候预估计一下多长合适,最好预估准确。
1.4 二维数组
上面提到的数组是一维数组,数组元素可以存储引用数据类型,那么当然也可以存储一维数组。这样形成的就是二维数组,二维数组其实就是特殊的一维数组。同理,三维数组就是特殊的二维数组。
其初始化语法如下:
1 | // 静态初始化 |
其中第一个中括号用于索引外层元素,第二个中括号用于索引指定外层数组元素中的内层数组元素。
1 | public class ArrayTest04 { |
2. String类
2.1 String基本介绍
该类是java.lang.String
类,表示字符串,属于引用数据类型。在Java中随便使用双引号括起来的都是String对象,例如“abc”、“hello world”等等,这些都是String对象。Java中规定,双引号括起来的字符串是不可变的,也就是说“abc”自出生到最终死亡都不可变,这个对象不会变成“abcd”。在JDK中双引号括起来的字符串,例如“abc”、“def”都是直接存储在方法区中的“字符串常量池”当中的,(感觉有点像字面值),注意,垃圾回收器是不会释放常量的。
为什么SUN公司把字符串存储在一个“字符串常量池”当中呢?因为字符串在实际的开发中使用太频繁。为了执行效率,所以把字符串放到了方法区的字符串常量池当中。因为字符串无法改变,所以后续用到相同字符串的时候,直接用该地址即可,无需再次创建。如下代码所示,字符串拼接会产生新的字符串对象:
1 | public class StringTest01 { |
所以String对象的两种创建形式在内存中是不一样的,一种是直接指向字符串常量池,另一种是指向堆内存,然后堆内存再指向字符串常量池。所以,用双等号“==”来判断两个String对象的内容是否相等是不保险的,如果对象是在堆中,则二者内存地址不一样,就会被认为不相等,一般用String类重写后的equals方法来比较二者是否相等。
2.2 String构造方法
最常用的构造方法有如下几种:
1 | // 1. 直接将字符串赋值 |
2.3 String常用方法
方法 | 功能 |
---|---|
public char charAt(int index) | 返回字符串中指定位置的字符 |
public int compareTo(String anotherString) | 按字典顺序比较两个字符串,返回0表示两个字符串相等,<0表示自身小于参数字符串,>0表示自身大于参数字符串(以ASCII码序号比较) |
public boolean contains(CharSequense s) | 判断字符串是否包含参数中的字符串 |
public boolean endsWith(String suffix) | 判断字符串是否以某个子串结尾。(同理,肯定有startsWith()方法) |
public boolean equals(Object anObject) | 判断两个字符串是否相等(值是否相等,并不是内存地址) |
public boolean equalsIgnoreCase(String anotherString) | 判断两个字符串是否相等,忽略大小写。 |
public byte[] getBytes() | 将字符串对象转换成byte数组。 |
public int indexOf(String str) | 返回参数字符串在自身字符串中第一次出现的索引。 |
public boolean isEmpty() | 判断某个字符串是否为空。 |
public int length() | 返回字符串的长度。 |
public String replace(CharSequence target, CharSequence replacement) | 将自身字符串中的target子串替换为replacement子串。 |
public String[] split(String regex) | 将自身字符串按照正则表达式或指定字符串分割,返回String类型数组 |
public String substring(int beginIndex, int endIndex) | 截取自身字符串,下标从beginIndex,到endIndex,右开区间。 |
public char[] toCharArray() | 将自身字符串转换为一个字符数组。 |
public String trim() | 去掉字符串前后空白。 |
public static String valueOf() | 静态方法,将参数“非字符串”转换为字符串,参数可以是任何数据类型,包括引用数据类型,调用其toString()方法。 |
注意,equals()方法和compareTo()方法有什么区别呢?本质上没有区别,都是比较的是字符串的值,无非就是返回结果的不同,或者说compareTo可以比较出具体的大小。另外,似乎旧版本的JDK,equals()方法内部就是调用的是compareTo()方法。
注意,valueOf()方法就是将其他数据类型的数据转换为字符串类型数据,此时可以查看System.out.println()方法的源码,它就是调用了valueOf方法。实际上显示在控制台上的东西都是字符串。
2.4 简单案例
1 | public class StringTest01 { |
3. StringBuffer类
上面提到过,字符串一旦在字符串常量池中出现,那么就不会改变,此时如果拼接字符串,就会生成新的字符串常量,这样会占用大量的方法区内存,造成内存空间的浪费。
所以如果需要大量的字符串拼接操作,此时可以用Java提供的StringBuffer类和StringBuilder类,二者均在java.lang包下。
其中StringBuffer是线程安全的,StringBuilder是线程不安全的。是否线程安全参考后续的多线程编程。
3.1 StringBuffer类
3.1.1 构造方法
构造方法名 | 作用 |
---|---|
StringBuffer() | 构造一个其中不带字符的字符串缓冲区,初始容量为16个字符。 |
StringBuffer(CharSequence seq) | 构造一个字符串缓冲区,包含指定的seq字符序列。 |
StringBuffer(int capacity) | 构造一个不带字符,但具有指定初始容量的字符串缓冲区。 |
StringBuffer(String str) | 构造一个字符串缓冲区,并将其内容初始化为指定的字符串。 |
通过源码可以发现,底层开辟了byte数组,后续将字符对应的ASCII码保存到数组中。并且当数组存储满了之后,可以自动扩容,而String类底层也是数组,但是final修饰,即不可变,无论是长度还是内容。
注意,StringBuffer节省空间只是节省的是中间产物,比如”a”连续追加拼接到“z”,最终生成的对象是26个单个字符和长度为26的字符串,而String类则除了上述对象,还会生成“ab”、“abc”等类似的中间产物对象。
另外,在创建StringBuffer的时候尽可能给定一个初始化容量,最好减少底层的扩容次数,预估一下,给一个大一些的初始化容量。
注意,尽管会初始化长度,但是这个长度并不是实际字符串的长度。
3.1.2 常用方法
apend()
主要用到的就是这个方法,用于字符串的拼接,并且,如果初始化设置的容量不够就会自动进行扩容。
3.2 StringBuilder类
和StringBuilder类类似,只不过该类中的方法没有synchronized关键字修饰,线程不安全。
3.3 简单案例
1 | public class StringBufferTest01 { |
4. Object类
4.1 Object类概述
Object类是Java类库所有类的根类,这个类中的方法需要研究一下,因为这些方法是所有子类通用的,任何一个类都默认继承Object,即使没有直接继承,最终也会间接继承。
4.2 Object常用方法
4.2.1 toString()
源码如下:
1 | public String toString(){ |
可以看到,源码方法的实现默认是类名@对象内存地址的十六进制。那么该方法的作用是什么呢?通过调用这个方法可以将一个“Java对象”转换成“字符串表示形式”。建议所有子类都重写这个方法,自定义想要显示的内容。
另外,如果System.out.println()中的参数是一个引用,那么就会自动调用该引用的toString()方法。
4.2.2 equals()
源码如下:
1 | public boolean equals(Object obj) { |
可以看到,源码方法的实现默认是用双等号,比较对象(引用数据类型)的内存地址是否相等。一般情况下,我们判断对象是否相等指的是其变量的值是否相等,并不是判断内存地址,所以Object中的equals()方法无法满足要求,一般情况下子类需要重写该方法,即自定义对象相等的判断标准。
注意,为了后续哈希表以及其他数据结构中的需要,如果一个类的equals方法重写了,那么hashCode()方法必须重写。并且equals方法返回如果是true,hashCode()方法返回的值必须一样。
4.2.3 finalize()
源码如下:
1 | protected void finalize() throws Throwable { } |
注意,这个方法protected关键字修饰的,只有一个方法体,里面没有代码。这个方法不需要程序员手动调用,JVM的垃圾回收器负责调用这个方法。
finalize()方法实际上是SUN公司为Java程序员准备的一个时机:垃圾回收时机。如果希望在对象销毁时机执行一段代码的话,这段代码要写到finalize()方法体当中。有点类似静态代码块,在指定实际执行执行的代码片段。
另外:System.gc()
方法可以建议启动垃圾回收器,即不让垃圾回收器按照原始设定启动,一定程序上建议它启动,即启动概率高了一些。
不过JDK9开始似乎已经弃用这个方法。
4.2.4 hashCode()
源码如下:
1 | public native int hashCode(); |
该方法不是抽象方法,有native修饰,底层调用C++程序。作用就是将一个Java对象的内存地址经过哈希算法,得到一个值。所以hashCode()方法的执行结果可以等同看做一个Java对象的内存地址。
4.2.5 clone()
源码如下:
1 | protected native Object clone() throws CloneNotSupportedException; |
底层调用C++,用于克隆对象,分为深克隆和浅克隆,此处略。
4.3 简单案例
简单案例如下
1 | class Animal{ |
5. Scanner类
前面提到过,程序本质上说是对数据的处理。而前面几篇文章中用到的数据都是我们在编写的时候提前写好的,这些数据是“静态”的,很多时候需要用户输入一些数据,那么用户如何输入数据呢,程序如何获取到这些数据呢?
这里简单介绍一个类,用于获取用户控制台的输入,先记住基本语法,具体的原理可参考Java进阶-05-IO流。该类是java.util.Scanner
类,创建对象时的参数为System.in,主要有nextInt()、hasNext()、next()、nextLine()等方法,注意,回车符号可能会存储在缓冲区中,所以上述方法尽量单独测试。
1 | import java.util.Scanner; |
6. Arrays类
这个类是数组工具类,里面提供了针对数组的很多方法,比如快速排序、冒泡排序、二分查找、计算哈希值等等,方法基本上都是静态的,可以直接调用。主要用到的方法是查找和排序。
简单代码如下所示:
1 | import java.util.Arrays; |
7. 包装类
7.1 包装类基本介绍
Java为8种基本数据类型又对应准备了8种包装类型。8种包装类型属于引用数据类型,父类是Object。有了基本数据类型,为什么要提供对应的包装类呢?
有时候有这种需求,方法的参数是Object类型,这种方法参数实现了多态,可以传入任何引用数据类型,但是此时却不能传入基本数据类型(现在有了自动装箱功能,所以目前测试不出来),这样可能就不会满足需求,基本数据类型没有父类。包装类的作用就是为了方便编程。
那么此时怎么做呢?可以将基本数据类型封装成引用数据类型,即创建一个类,里面的属性是某一个基本数据类型变量,继承Object类。Java中提供了这种封装基本数据类型的包装类。基本数据类型和包装类型的对应如下所示:
基本数据类型 | 包装类型 |
---|---|
byte | java.lang.Byte(父类Number) |
short | java.lang.Short(父类Number) |
int | java.lang.Integer(父类Number) |
long | java.lang.Long(父类Number) |
float | java.lang.Float(父类Number) |
double | java.lang.Double(父类Number) |
boolean | java.lang.Boolean(父类Object) |
char | java.lang.Character(父类Object) |
八种包装类型中,6个类的父类是Number类,2个类的父类是Object。下面先介绍一下java.lang.Number。
7.2 Number类
Number类是一个抽象类,代码如下:
1 | public abstract class Number implements Serializable { |
提供的抽象方法可以看到,均是将数值转换指定的基本数据类型值,也可认为是将引用数据类型转换为基本数据类型。这里引出两个概念:
- 装箱:将基本数据类型转换成引用数据类型。
- 拆箱:将引用数据类型转换成基本数据类型。
所以,包装类构造方法用于装箱,其继承的Number类中的部分方法用于拆箱。但是有了自动装箱和自动拆箱,实际上Number中的方法就不需要显式调用了。
7.3 java.lang.Integer类
7.3.1 构造方法
该类的构造方法在JDK9之后弃用了,但是也可以用。除了构造方法,可以直接用对应的基本数据类型数值赋值该引用(自动装箱)。另外,包装类也提供了最大值最小值等常量。
1 | System.out.println("Integer类型:"); |
7.3.2 自动装箱和自动拆箱
在JDK1.5之后,对基本数据类型及其包装类支持自动装箱和自动拆箱了。
自动装箱指的将基本数据类型自动转换成包装类。自动拆箱指的是将包装类自动转换成基本数据类型。涉及到加减乘除等运算的时候会触发自动装箱和拆箱机制,双等号判断的时候JDK旧版本不会触发,最新版本可能会触发。
1 | Integer x = 600; // 自动装箱 |
注意,在[-128, 127]之间,这些数值用的比较多,所以Java为了提高程序的执行效率,将这些数值之间的所有包装对象提前创建好,放到了一个方法区的“整数型常量池”当中了,目的是只要这个区间的数据不需要再new了,和字符串常量池一样,直接取。所以此时双等号判断会为true,不要认为双等号这里采用了拆箱。
1 | Integer aa = 134; |
参考Integer类源码中的静态内部类,如下所示,自动创建 [-128, 127]之间的缓存对象。
1 | private static class IntegerCache { |
7.3.3 Integer常用方法
public int intValue()
手动拆箱,即返回Int类型的对应值。
public static int parseInt(String s)
将字符串转换成int类型,前提是字符串必须是数字型字符串,相当于python中的eval()函数。
public static Integer valueOf(int i)
将int类型数据转换成对应的Integer类型数据。
public static Integer valueOf(String s)
将String类型数据转换成对应的Integer类型数据。前提是String类型数据必须是对应的数值所形成的数据,如”1223”等可以转换成Integer类型的字符串。
7.3.4 String、int、Integer三种类型互相转换
7.4 补充
Integer类型的相关方法如上所述,其他七种包装类型也是相类似的方法。一般情况下采用自动装箱即可。
8. 其他工具类
8.1 Date类
该类是Java提供的日期类,提供对日期的一些常见操作,该类全称是java.util.Date
,可以获取当前日期等相关操作。
另外,由于日期类的输出和中国人日常格式不同,所以java.text.SimpleDateFormat
类提供了对日期的格式操作,按照指定格式格式化日期对象并返回字符串。同时,该类也提供了将字符串转换成日期对象的方法。
System.currentTimeMillis()
方法可以获取系统当前时间距离1970年1月1日之间的毫秒数,那么统计程序运行时长可以两次调用该方法,取差值即可。
8.2 Random类
java.util.Random
类是Java提供的随机数类,提供随机数的相关操作。
8.3 enum关键字
java.lang.Enum
类是Java提供的抽象类:枚举类。enum关键字意味着所创建的类型都是java.lang.Enum
类的子类。
枚举类型有什么用处呢?定义变量的时候,因为变量可以随意取值,也就是有无限种取值情况,但是有时候,我们其实只需要其中几种取值,那么此时,如果单纯的只定义这几种取值,比如value
的取值只有0、1、2三种,有可能会写错,并且程序运行也没什么问题。可以限定value的取值,这时候就不会写错。
枚举就是一枚一枚可以列举出来的,才建议使用枚举类型,如果取值只有两种情况,建议使用布尔类型,超过两种并且还可以列举出来,建议使用枚举类型,比如颜色、四季、星期等等。
和interface、class一样,作为关键字,如enum Result{}
,可以单独写入一个文件。枚举编译之后也生成class文件,枚举也是一种引用数据类型,枚举中的每一个值可以看做是常量。
注意,不能实例化,只能采用类名.常量
的形式调用。
语法如下所示:
1 | enum 枚举类型名{ |
可以将枚举看成一个类,枚举值就是其中的静态常量,例子如下所示:
1 | public class EnumTest01 { |
补充一个IDEA中的提示文件,在选中包的情况下,按ALT+Insert
弹出可创建的文件类型,就包含Enum类型。
8.4 DecimalFormat和BigDecimal类
java.text.DecimalFormat
是Java提供的专门负责数字格式化的类,其中**#代表任意数字,’,’代表千分位,’.’代表小数点**。
java.math.BigDecimal
是Java提供的大数据类型,精度极高,是引用数据类型,专门用在财务软件当中。财务软件中double类型是不够的,需要用到BigDecimal。
9. 补充
9.1 java.util包
可以看到,上述的很多类都是在java.util
包下,这个包是Java提供的工具包,里面封装了很多Java提供的工具类,比如常见的数据结构以及其他类。
9.2 java.lang包
该包下的直接子类不需要导入,Java会自动导入。著名的System
、Object
等类都在此包下。
10. 备注
参考B站《动力节点》。