面向对象高级篇
经过前面的学习,我们已经了解了面向对象编程的大部分基础内容,这一部分,我们将继续探索面向对象编程过程中一些常用的东西。
基本类型包装类
Java并不是纯面向对象的语言,虽然Java语言是一个面向对象的语言,但是Java中的基本数据类型却不是面向对象的。Java中的基本类型,如果想通过对象的形式去使用他们,Java提供的基本类型包装类,使得Java能够更好的体现面向对象的思想,同时也使得基本类型能够支持对象操作!
包装类介绍
所有的包装类层次结构如下:
其中能够表示数字的基本类型包装类,继承自Number类,对应关系如下表:
- byte -> Byte
- boolean -> Boolean
- short -> Short
- char -> Character
- int -> Integer
- long -> Long
- float -> Float
- double -> Double
我们可以直接使用,这里我们以Integer类为例:
public static void main(String[] args) {
Integer i = new Integer(10); //将10包装为一个Integer类型的变量
}
包装类实际上就是将我们的基本数据类型,封装成一个类(运用了封装的思想)我们可以来看看Integer类中是怎么写的:
private final int value; //类中实际上就靠这个变量在存储包装的值
public Integer(int value) {
this.value = value;
}
包装类型支持自动装箱,我们可以直接将一个对应的基本类型值作为对应包装类型引用变量的值:
public static void main(String[] args) {
Integer i = 10; //将int类型值作为包装类型使用
}
这是怎么做到的?为什么一个对象类型的值可以直接接收一个基本类类型的值?实际上这里就是自动装箱:
public static void main(String[] args) {
Integer i = Integer.valueOf(10); //上面的写法跟这里是等价的
}
这里本质上就是被自动包装成了一个Integer类型的对象,只是语法上为了简单,就支持像这样编写。既然能装箱,也是支持拆箱的:
public static void main(String[] args) {
Integer i = 10;
int a = i;
}
实际上上面的写法本质上就是:
public static void main(String[] args) {
Integer i = 10;
int a = i.intValue(); //通过此方法变成基本类型int值
}
这里就是自动拆箱,得益于包装类型的自动装箱和拆箱机制,我们可以让包装类型轻松地参与到基本类型的运算中:
public static void main(String[] args) {
Integer a = 10, b = 20;
int c = a * b; //直接自动拆箱成基本类型参与到计算中
System.out.println(c);
}
因为包装类是一个类,不是基本类型,所以说两个不同的对象,那么是不相等的:
public static void main(String[] args) {
Integer a = new Integer(10);
Integer b = new Integer(10);
System.out.println(a == b); //虽然a和b的值相同,但是并不是同一个对象,所以说==判断为假
}
那么自动装箱的呢?
public static void main(String[] args) {
Integer a = 10, b = 10;
System.out.println(a == b);
}
我们发现,通过自动装箱转换的Integer对象,如果值相同,得到的会是同一个对象,这是因为:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) //这里会有一个IntegerCache,如果在范围内,那么会直接返回已经提前创建好的对象
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache会默认缓存-128~127之间的所有值,将这些值提前做成包装类放在数组中存放,虽然我们目前还没有学习数组,但是各位小伙伴只需要知道,我们如果直接让 -128~127之间的值自动装箱为Integer类型的对象,那么始终都会得到同一个对象,这是为了提升效率,因为小的数使用频率非常高,有些时候并不需要创建那么多对象,创建对象越多,内存也会消耗更多。
但是如果超出这个缓存范围的话,就会得到不同的对象了:
public static void main(String[] args) {
Integer a = 128, b = 128;
System.out.println(a == b);
}
这样就不会得到同一个对象了,因为超出了缓存的范围。同样的,Long、Short、Byte类型的包装类也有类似的机制,感兴趣的小伙伴可以自己点进去看看。
我们来看看包装类中提供了哪些其他的方法,包装类支持字符串直接转换:
public static void main(String[] args) {
Integer i = new Integer("666"); //直接将字符串的666,转换为数字666
System.out.println(i);
}
当然,字符串转Integer有多个方法:
public static void main(String[] args) {
Integer i = Integer.valueOf("5555");
//Integer i = Integer.parseInt("5555");
System.out.println(i);
}
我们甚至可以对十六进制和八进制的字符串进行解码,得到对应的int值:
public static void main(String[] args) {
Integer i = Integer.decode("0xA6");
System.out.println(i);
}
也可以将十进制的整数转换为其他进制的字符串:
public static void main(String[] args) {
System.out.println(Integer.toHexString(166));
}
当然,Integer中提供的方法还有很多,这里就不一一列出了。
特殊包装类
除了我们上面认识的这几种基本类型包装类之外,还有两个比较特殊的包装类型。
其中第一个是用于计算超大数字的BigInteger,我们知道,即使是最大的long类型,也只能表示64bit的数据,无法表示一个非常大的数,但是BigInteger没有这些限制,我们可以让他等于一个非常大的数字:
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE); //表示Long的最大值,轻轻松松
System.out.println(i);
}
我们可以通过调用类中的方法,进行运算操作:
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE);
i = i.multiply(BigInteger.valueOf(Long.MAX_VALUE)); //即使是long的最大值乘以long的最大值,也能给你算出来
System.out.println(i);
}
我们来看看结果:
可以看到,此时数值已经非常大了,也可以轻松计算出来。咱们来点更刺激的:
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE);
i = i.pow(100); //long的最大值来个100次方吧
System.out.println(i);
}
可以看到,这个数字已经大到一排显示不下了:
一般情况,对于非常大的整数计算,我们就可以使用BigInteger来完成。
我们接着来看第二种,前面我们说了,浮点类型精度有限,对于需要精确计算的场景,就没办法了,而BigDecimal可以实现小数的精确计算。
public static void main(String[] args) {
BigDecimal i = BigDecimal.valueOf(10);
i = i.divide(BigDecimal.valueOf(3), 100, RoundingMode.CEILING);
//计算10/3的结果,精确到小数点后100位
//RoundingMode是舍入模式,就是精确到最后一位时,该怎么处理,这里CEILING表示向上取整
System.out.println(i);
}
可以看到,确实可以精确到这种程度:
但是注意,对于这种结果没有终点的,无限循环的小数,我们必须要限制长度,否则会出现异常。
数组
我们接着来看一个比较特殊的类型,数组。
假设出现一种情况,我们想记录100个数字,要是采用定义100个变量的方式可以吗?是不是有点太累了?这种情况我们就可以使用数组来存放一组相同类型的数据。
一维数组
数组是相同类型数据的有序集合,数组可以代表任何相同类型的一组内容(包括引用类型和基本类型)其中存放的每一个数据称为数组的一个元素,我们来看看如何去定义一个数组变量:
public static void main(String[] args) {
int[] array; //类型[]就表示这个是一个数组类型
}
注意,数组类型比较特殊,它本身也是类,但是编程不可见(底层C++写的,在运行时动态创建)即使是基本类型的数组,也是以对象的形式存在的,并不是基本数据类型。所以,我们要创建一个数组,同样需要使用new
关键字:
public static void main(String[] args) {
int[] array = new int[10]; //在创建数组时,需要指定数组长度,也就是可以容纳多个int变量的值
Object obj = array; //因为同样是类,肯定是继承自Object的,所以说可以直接向上转型
}
除了上面这种方式之外,我们也可以使用其他方式:
类型[] 变量名称 = new 类型[数组大小];
类型 变量名称[] = new 类型[数组大小]; //支持C语言样式,但不推荐!
类型[] 变量名称 = new 类型[]{...}; //静态初始化(直接指定值和大小)
类型[] 变量名称 = {...}; //同上,但是只能在定义时赋值
创建出来的数组每个位置上都有默认值,如果是引用类型,就是null,如果是基本数据类型,就是0,或者是false,跟对象成员变量的默认值是一样的,要访问数组的某一个元素,我们可以:
public static void main(String[] args) {
int[] array = new int[10];
System.out.println("数组的第一个元素为:"+array[0]); //使用 变量名[下标] 的方式访问
}
注意,数组的下标是从0开始的,不是从1开始的,所以说第一个元素的下标就是0,我们要访问第一个元素,那么直接输入0就行了,但是注意千万别写成负数或是超出范围了,否则会出现异常。
我们也可以使用这种方式为数组的元素赋值:
public static void main(String[] args) {
int[] array = new int[10];
array[0] = 888; //就像使用变量一样,是可以放在赋值运算符左边的,我们可以直接给对应下标位置的元素赋值
System.out.println("数组的第一个元素为:"+array[0]);
}
因为数组本身也是一个对象,数组对象也是具有属性的,比如长度:
public static void main(String[] args) {
int[] array = new int[10];
System.out.println("当前数组长度为:"+array.length); //length属性是int类型的值,表示当前数组长度,长度是在一开始创建数组的时候就确定好的
}
注意,这个length
是在一开始就确定的,而且是final
类型的,不允许进行修改,也就是说数组的长度一旦确定,不能随便进行修改,如果需要使用更大的数组,只能重新创建。
当然,既然是类型,那么肯定也是继承自Object类的:
public static void main(String[] args) {
int[] array = new int[10];
System.out.println(array.toString());
System.out.println(array.equals(array));
}
但是,很遗憾,除了clone()之外,这些方法并没有被重写,也就是说依然是采用的Object中的默认实现:
所以说通过toString()
打印出来的结果,好丑,只不过我们可以发现,数组类型的类名很奇怪,是[
开头的。
因此,如果我们要打印整个数组中所有的元素,得一个一个访问:
public static void main(String[] args) {
int[] array = new int[10];
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
有时候为了方便,我们可以使用简化版的for语句foreach
语法来遍历数组中的每一个元素:
public static void main(String[] args) {
int[] array = new int[10];
for (int i : array) { //int i就是每一个数组中的元素,array就是我们要遍历的数组
System.out.print(i+" "); //每一轮循环,i都会更新成数组中下一个元素
}
}
是不是感觉这种写法更加简洁?只不过这仅仅是语法糖而已,编译之后依然是跟上面一样老老实实在遍历的:
public static void main(String[] args) { //反编译的结果
int[] array = new int[10];
int[] var2 = array;
int var3 = array.length;
for(int var4 = 0; var4 < var3; ++var4) {
int i = var2[var4];
System.out.print(i + " ");
}
}
对于这种普通的数组,其实使用还是挺简单的。这里需要特别说一下,对于基本类型的数组来说,是不支持自动装箱和拆箱的:
public static void main(String[] args) {
int[] arr = new int[10];
Integer[] test = arr;
}
还有,由于基本数据类型和引用类型不同,所以说int类型的数组时不能被Object类型的数组变量接收的:
但是如果是引用类型的话,是可以的:
public static void main(String[] args) {
String[] arr = new String[10];
Object[] array = arr; //数组同样支持向上转型
}
public static void main(String[] args) {
Object[] arr = new Object[10];
String[] array = (String[]) arr; //也支持向下转型
}
多维数组
前面我们介绍了简单的数组(一维数组)既然数组可以是任何类型的,那么我们能否创建数组类型的数组呢?答案是可以的,套娃嘛,谁不会:
public static void main(String[] args) {
int[][] array = new int[2][10]; //数组类型数组那么就要写两个[]了
}
存放数组的数组,相当于将维度进行了提升,比如上面的就是一个2x10的数组:
这个中数组一共有2个元素,每个元素都是一个存放10个元素的数组,所以说最后看起来就像一个矩阵一样。甚至可以继续套娃,将其变成一个三维数组,也就是存放数组的数组的数组。
public static void main(String[] args) {
int[][] arr = { {1, 2},
{3, 4},
{5, 6}}; //一个三行两列的数组
System.out.println(arr[2][1]); //访问第三行第二列的元素
}
在访问多维数组时,我们需要使用多次[]
运算符来得到对应位置的元素。如果我们要遍历多维数组话,那么就需要多次嵌套循环:
public static void main(String[] args) {
int[][] arr = new int[][]{{1, 2},
{3, 4},
{5, 6}};
for (int i = 0; i < 3; i++) { //要遍历一个二维数组,那么我们得一列一列一行一行地来
for (int j = 0; j < 2; j++) {
System.out.println(arr[i][j]);
}
}
}
可变长参数
我们接着来看数组的延伸应用,实际上我们的方法是支持可变长参数的,什么是可变长参数?
public class Person {
String name;
int age;
String sex;
public void test(String... strings){
}
}
我们在使用时,可以传入0 - N个对应类型的实参:
public static void main(String[] args) {
Person person = new Person();
person.test("1!", "5!", "哥们在这跟你说唱"); //这里我们可以自由传入任意数量的字符串
}
那么我们在方法中怎么才能得到这些传入的参数呢,实际上可变长参数本质就是一个数组:
public void test(String... strings){ //strings这个变量就是一个String[]类型的
for (String string : strings) {
System.out.