数组是相同类型数据的有序集合,数组可以代表任何相同类型的一组内容(包括引用类型和基本类型)其中存放的每一个数据称为数组的一个元素,我们来看看如何去定义一个数组变量:
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.String());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 = 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]);}}
}
实际上方法是支持可变长参数的,什么是可变长参数?
ample;
public class Test {public void strings){}
}
我们在使用时,可以传入0 - N个对应类型的实参:
ample;
public class Main {public static void main(String[] args) {Test test = new Test();test.t("hi","你好","hello","world");}
}
那么我们在方法中怎么才能得到这些传入的参数呢,实际上可变长参数本质就是一个数组:
ample;
public class Test {public void strings) { //strings就是个String[]类型的变量for (String string : strings) {System.out.println(string); //遍历打印数组中每一个元素}}
}
注意,如果同时存在其他参数,那么可变长参数只能放在最后:
public class Test {public void t(int a,int strings) { }
}
这里最后我们再来说一个从开始到现在一直都没有说的东西:
public static void main(String[] args) { //这个String[] args到底是个啥???
}
实际上这个是我们在执行Java程序时,输入的命令行参数,我们可以来打印一下:
public static void main(String[] args) {for (String arg : args) {System.out.println(arg);}
}
可以看到,默认情况下直接运行什么都没有,但是如果我们在运行时,添加点内容的话:
可以看到,我们在后面随意添加的三个参数,都放到数组中了:
这个东西我们作为新手一般也不会用到,只做了解就行了。
String本身也是一个类,只不过它比较特殊,每个用双引号括起来的字符串,都是String类型的一个实例对象:
public static void main(String[] args) {String str = "Hello World!";
}
我们也可以象征性地使用一下new关键字:
public static void main(String[] args) {String str = new String("Hello World!"); //这种方式就是创建一个新的对象
}
注意,如果是直接使用双引号创建的字符串,如果内容相同,为了优化效率,那么始终都是同一个对象:输出为true
public static void main(String[] args) {String str1 = "Hello World";String str2 = "Hello World";System.out.println(str1 == str2);
}
但是如果我们使用构造方法主动创建两个新的对象,那么就是不同的对象了:输出为false
public static void main(String[] args) {String str1 = new String("Hello World");String str2 = new String("Hello World");System.out.println(str1 == str2);
}
因此,如果我们仅仅是想要判断两个字符串的内容是否相同,不要使用==
,String类重载了equals
方法用于判断和比较内容是否相同:
public static void main(String[] args) {String str1 = new String("Hello World");String str2 = new String("Hello World");System.out.println(str1.equals(str2)); //字符串的内容比较,一定要用equals
}
既然String也是一个类,那么肯定是具有一些方法的,我们可以来看看:
public static void main(String[] args) {String str = "Hello World";System.out.println(str.length()); //length方法可以求字符串长度,这个长度是字符的数量
}
因为双引号括起来的字符串本身就是一个实例对象,所以说我们也可以直接用:
public static void main(String[] args) {System.out.println("Hello World".length()); //虽然看起来挺奇怪的,但是确实支持这种写法
}
字符串类中提供了很多方便我们操作的方法,比如字符串的裁剪、分割操作等:
public static void main(String[] args) {String str = "Hello World";String sub = str.substring(0, 3);//0,1,2,前闭后开的区间//分割字符串,并返回一个新的子串对象System.out.println(sub);
}
public static void main(String[] args) {String str = "Hello World";String[] strings = str.split(" "); //使用split方法进行字符串分割,比如这里就是通过空格分隔,得到一个字符串数组for (String string : strings) {System.out.println(string);}
}
对字符串的分割,转换都不是对原有字符串修改,而是返回一个新的字符串对象 :
public static void main(String[] args) {String str = "Hello World!";System.out.LowerCase());//所有大写字母转换为小写字母System.out.println(str);//打印第0个字符
}
运行结果:
hello world!
Hello World!进程已结束,退出代码0
字符数组和字符串之间是可以快速进行相互转换的:
public static void main(String[] args) {String str = "Hello World";char[] chars = CharArray();System.out.println(chars);
}
public static void main(String[] args) {char[] chars = new char[]{'奥', '利', '给'};String str = new String(chars);System.out.println(str);
}
一些常用的方法:
public static void main(String[] args) {String str = "hello world!";System.out.ains("hello"));//str中是否包含hello
}public static void main(String[] args) {String str = "hello world!";System.out.println(str.charAt(0));//打印第0个字符
}public static void main(String[] args) {String str = "Hello World!";System.out.println(str.startsWith("H"));//字符串是否以H开头
}public static void main(String[] args) {String str = "Hello World!";System.out.println(str.indexOf("World"));//所包含字符串的位置,不存在返回-1
}public static void main(String[] args) {String str = "Hello World!";System.out.place("Hello","hi"));//将Hello替换为hi
}
当然,String类还有很多其他的一些方法,这里就不一一介绍了。
字符串支持使用 +
和+=
进行拼接操作,但是拼接字符串实际上底层需要进行很多操作,如果程序中大量进行字符串的拼接似乎不太好,编译器是很聪明的,String的拼接会在编译时进行各种优化:
public static void main(String[] args) {String str = "杰哥" + "你干嘛"; //我们在写代码时使用的是拼接的形式System.out.println(str);
}
编译之后就变成这样了:
public static void main(String[] args) {String str = "杰哥你干嘛";System.out.println(str);
}
对于变量来说,也有优化,比如下面这种情况:
public static void main(String[] args) {String str1 = "你看";String str2 = "这";String str3 = "汉堡";String str4 = "做滴";String str5 = "行不行";String result = str1 + str2 + str3 + str4 + str5; //5个变量连续加System.out.println(result);
}
如果直接使用加的话,每次运算都会生成一个新的对象,这里进行4次加法运算,那么中间就需要产生4个字符串对象出来,是不是有点太浪费了?这种情况实际上会被优化为下面的写法:
public static void main(String[] args) {String str1 = "你看";String str2 = "这";String str3 = "汉堡";String str4 = "做滴";String str5 = "行不行";StringBuilder builder = new StringBuilder();builder.append(str1).append(str2).append(str3).append(str4).append(str5);System.out.String());
}
这里创建了一个StringBuilder的类型,这个类型是干嘛的呢?实际上它就是专门用于构造字符串的,我们可以使用它来对字符串进行拼接、裁剪等操作,它就像一个字符串编辑器,弥补了字符串不能修改的不足:
public static void main(String[] args) {StringBuilder builder = new StringBuilder(); //一开始创建时,内部什么都没有builder.append("AAA"); //我们可以使用append方法来讲字符串拼接到后面builder.append("BBB");System.out.String()); //当我们字符串编辑完成之后,就可以使用toString转换为字符串了
}
StringBuilder是可以对字符串修改的,它还支持裁剪等操作:
public static void main(String[] args) {StringBuilder builder = new StringBuilder("AAABBB"); //在构造时也可以指定初始字符串builder.delete(2, 4); //删除2到4这个范围内的字符System.out.String());
}
当然,StringBuilder类的编辑操作也非常多,这里就不一一列出了。
我们现在想要实现这样一个功能,对于给定的字符串进行判断,如果字符串符合我们的规则,那么就返回真,否则返回假,比如现在我们想要判断字符串是不是邮箱的格式:
public static void main(String[] args) {String str = "982292129@qq";//假设邮箱格式为 数字/字母@数字/字母
}
那么现在请你设计一个Java程序用于判断,你该怎么做?是不是感觉很麻烦,但是我们使用正则表达式就可以很轻松解决这种字符串格式匹配问题。
正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。
我们先来看看下面的这个例子:public static void main(String[] args) {String str = "oooo";//matches方法用于对给定正则表达式进行匹配,匹配成功返回true,否则返回falseSystem.out.println(str.matches("o+")); //+表示对前面这个字符匹配一次或多次,这里字符串是oooo,正好可以匹配
}
用于规定给定组件必须要出现多少次才能满足匹配的,我们一般称为限定符,限定符表如下:
字符 | 描述 |
---|---|
* | 匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于 {0,}。 |
+ | 匹配前面的子表达式一次或多次。例如,zo+ 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。 |
? | 匹配前面的子表达式零次或一次。例如,do(es)? 可以匹配 "do" 、 "does"、 "doxy" 中的 "do" 。? 等价于 {0,1}。 |
{n} | n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 "Bob" 中的 o,但是能匹配 "food" 中的两个 o。 |
{n,} | n 是一个非负整数。至少匹配n 次。例如,o{2,} 不能匹配 "Bob" 中的 o,但能匹配 "foooood" 中的所有 o。o{1,} 等价于 o+。o{0,} 则等价于 o*。 |
{n,m} | m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 "fooooood" 中的前三个 o。o{0,1} 等价于 o?。请注意在逗号和两个数之间不能有空格。 |
如果我们想要表示一个范围内的字符,可以使用方括号:
public static void main(String[] args) {String str = "abcabccaa";System.out.println(str.matches("[abc]*")); //表示abc这几个字符可以出现 0 - N 次
}
对于普通字符来说,我们可以下面的方式实现多种字符匹配:
字符 | 描述 |
---|---|
[ABC] | 匹配 [...] 中的所有字符,例如 [aeiou] 匹配字符串 "google runoob taobao" 中所有的 e o u a 字母。 |
[^ABC] | 匹配除了 [...] 中字符的所有字符,例如 [^aeiou] 匹配字符串 "google runoob taobao" 中除了 e o u a 字母的所有字母。 |
[A-Z] | [A-Z] 表示一个区间,匹配所有大写字母,[a-z] 表示所有小写字母。 |
. | 匹配除换行符(n、r)之外的任何单个字符,相等于 [^nr] |
[sS] | 匹配所有。s 是匹配所有空白符,包括换行,S 非空白符,不包括换行。 |
w | 匹配字母、数字、下划线。等价于 [A-Za-z0-9_] |
当然,这里仅仅是对正则表达式的简单使用,实际上正则表达式内容非常多,如果需要完整学习正则表达式,可以到:正则表达式 – 语法 | 菜鸟教程
正则表达式并不是只有Java才支持,其他很多语言比如JavaScript、Python等等都是支持正则表达式的。
上一章我们详细介绍了类,我们现在已经知道该如何创建类、使用类了。当然,类的创建其实可以有多种多样的方式,并不仅仅局限于普通的创建。内部类顾名思义,就是创建在内部的类,那么具体是什么的内部呢,我们接着就来讨论一下。
注意:内部类很多地方都很绕,所以说一定要仔细思考。
我们可以直接在类的内部定义成员内部类:
public class Test {public class Inner {
//内部类也是类,所以说里面也可以有成员变量、方法等,甚至还可以继续套娃一个成员内部类public void test(){System.out.println("我是成员内部类!");}}
}
成员内部类和成员方法、成员变量一样,是对象所有的,而不是类所有的,如果我们要使用成员内部类,那么就需要:
public static void main(String[] args) {Test test = new Test(); //我们首先需要创建对象Test.Inner inner = w Inner(); //成员内部类的类型名称就是 外层.内部类名称
}
虽然看着很奇怪,但是确实是这样使用的。我们同样可以使用成员内部类中的方法:
public static void main(String[] args) {Test test = new Test();Test.Inner inner = w Inner();st();
}
注意,成员内部类也可以使用访问权限控制,如果我们我们将其权限改为private
,那么就像我们把成员变量访问权限变成私有一样,外部是无法访问到这个内部类的:
可以看到这里直接不能识别了。
这里我们需要特别注意一下,在成员内部类中,是可以访问到外层的变量的:
public class Test {private final String name;public Test(String name){this.name = name;}public class Inner {public void test(){System.out.println("我是成员内部类:"+name);//成员内部类可以访问到外部的成员变量//因为成员内部类本身就是某个对象所有的,每个对象都有这样的一个类定义//这里的name是其所依附对象的}}
}
每个类可以创建一个对象,每个对象中都有一个单独的类定义,可以通过这个成员内部类又创建出更多对象,套娃了属于是。
所以说我们在使用时:
public static void main(String[] args) {Test a = new Test("小明");Test.Inner inner1 = a.new Inner(); //依附于a创建的对象,那么就是a的st();
Test b = new Test("小红");Test.Inner inner2 = b.new Inner(); //依附于b创建的对象,那么就是b的st();
}
那现在问大家一个问题,外部能访问内部类里面的成员变量吗?
很明显是不能的,我们并没有创建内部类的对象。
另外java8是不支持非静态内部类使用static声明的:
那么如果内部类中也定义了同名的变量,此时我们怎么去明确要使用的是哪一个呢?
public class Test {private final String name;public Test(String name){this.name = name;}public class Inner {String name;public void test(String name){System.out.println("方法参数的name = "+name); //依然是就近原则,最近的是参数,那就是参数了System.out.println("成员内部类的name = "+this.name); //在内部类中使用this关键字,只能表示内部类对象System.out.println("外部类的name = "+Test.this.name);//如果需要指定为外部的对象,那么需要在前面添加外部类型名称}}
}
包括对方法的调用和super关键字的使用,也是一样的:
public class Inner {String name;public void test(String name){String(); //内部类自己的toString方法String(); //内部类父类的toString方法String(); //外部类的toSrting方法String(); //外部类父类的toString方法}
}
所以说成员内部类其实在某些情况下使用起来比较麻烦,对于这种成员内部类,我们一般只会在类的内部自己使用。
前面我们介绍了成员内部类,它就像成员变量和成员方法一样,是属于对象的,同样的,静态内部类就像静态方法和静态变量一样,是属于类的,我们可以直接创建使用。
public class Test {private final String name;
public Test(String name){this.name = name;}
public static class Inner {public void test(){System.out.println("我是静态内部类!");}}
}
不需要依附任何对象,我们可以直接创建静态内部类的对象:
public static void main(String[] args) {Test.Inner inner = new Test.Inner(); //静态内部类的类名同样是之前的格式,但是可以直接new了st();
}
静态内部类由于是静态的,所以相对外部来说,整个内部类中都处于静态上下文(注意只是相当于外部来说)是无法访问到外部类的非静态内容的
只不过受影响的只是外部内容的使用,内部不受影响,还是跟普通的类一样:
public static class Inner {String name;public void test(){System.out.println("我是静态内部类:"+name);}
}
其实也很容易想通,因为静态内部类是属于外部类的,不依附任何对象,那么我要是直接访问外部类的非静态属性,那到底访问哪个对象的呢?这样肯定是说不通的。
局部内部类就像局部变量一样,可以在方法中定义。
public class Test {private final String name;
public Test(String name){this.name = name;}
public void hello(){class Inner { //直接在方法中创建局部内部类 }}
}
既然是在方法中声明的类,那作用范围也就只能在方法中了:
public class Test {public void hello(){class Inner{ //局部内部类跟局部变量一样,先声明后使用public void test(){System.out.println("我是局部内部类");}}Inner inner = new Inner(); //局部内部类直接使用类名就行st();}
}
只不过这种局部内部类的形式,使用频率很低,基本上不会用到,所以说了解就行了。
匿名内部类是我们使用频率非常高的一种内部类,它是局部内部类的简化版。
还记得我们在之前学习的抽象类和接口吗?在抽象类和接口中都会含有某些抽象方法需要子类去实现,我们当时已经很明确地说了不能直接通过new的方式去创建一个抽象类或是接口对象,但是我们可以使用匿名内部类。
public abstract class Student {public abstract void test();
}
正常情况下,要创建一个抽象类的实例对象,只能对其进行继承,先实现未实现的方法,然后创建子类对象。
而我们可以在方法中使用匿名内部类,将其中的抽象方法实现,并直接创建实例对象:
public static void main(String[] args) {Student student = new Student() { //在new的时候,后面加上花括号,把未实现的方法实现了@Overridepublic void test() {System.out.println("我是匿名内部类的实现!");}};st();
}
此时这里创建出来的Student对象,就是一个已经实现了抽象方法的对象,这个抽象类直接就定义好了,甚至连名字都没有,就可以直接就创出对象。
匿名内部类中同样可以使用类中的属性(因为它本质上就相当于是对应类型的子类)所以说:
Student student = new Student() {int a; //因为本质上就相当于是子类,所以说子类定义一些子类的属性完全没问题@Overridepublic void test() {System.out.println(name + "我是匿名内部类的实现!"); //直接使用父类中的name变量}
};
同样的,接口也可以通过这种匿名内部类的形式,直接创建一个匿名的接口实现类:
public static void main(String[] args) {Study study = new Study() {@Overridepublic void study() {System.out.println("我是学习方法!");}};study.study();
}
当然,并不是说只有抽象类和接口才可以像这样创建匿名内部类,普通的类也可以,只不过意义不大,一般情况下只是为了进行一些额外的初始化工作而已。
前面介绍了匿名内部类,我们可以通过这种方式创建一个临时的实现子类。
特别的,如果一个接口中有且只有一个待实现的抽象方法,那么我们可以将匿名内部类简写为Lambda表达式:
public static void main(String[] args) {Study study = () -> System.out.println("我是学习方法!"); //是不是感觉非常简洁!study.study();
}
在初学阶段,为了简化学习,就认为Lambda表达式就是匿名内部类的简写就行了(Lambda表达式的底层其实并不只是简简单单的语法糖替换,感兴趣的可以在新特性中了解)
那么它是一个怎么样的简写规则呢?我们来看一下Lambda表达式的具体规范:
标准格式为:([参数类型 参数名称,]...) ‐> { 代码语句,包括返回值 }
和匿名内部类不同,Lambda仅支持接口,不支持抽象类
接口内部必须有且仅有一个抽象方法(可以有多个方法,但是必须保证其他方法有默认实现,必须留一个抽象方法出来)
比如我们之前写的Study接口,只要求实现一个无参无返回值的方法,所以说直接就是最简单的形式:
() -> System.out.println("我是学习方法!");
//跟之前流程控制一样,如果只有一行代码花括号可省略
当然,如果有一个参数和返回值的话:
public static void main(String[] args) {Study study = (a) -> {System.out.println("我是学习方法");return "今天学会了"+a; //实际上这里面就是方法体,该咋写咋写};System.out.println(study.study(10));
}
注意,如果方法体中只有一个返回语句,可以直接省去花括号和return
关键字:
Study study = (a) -> {return "今天学会了"+a; //这种情况是可以简化的
};
Study study = (a) -> "今天学会了"+a;
如果参数只有一个,那么可以省去小括号:
Study study = a -> "今天学会了"+a;
是不是感觉特别简洁,实际上我们程序员追求的就是写出简洁高效的代码,而Java也在朝这个方向一直努力,近年来从Java 9开始出现的一些新语法基本都是各种各样的简写版本。
如果一个方法的参数需要的是一个接口的实现:
public static void main(String[] args) {test(a -> "今天学会了"+a); //参数直接写成lambda表达式
}
private static void test(Study study){study.study(10);
}
当然,这还只是一部分,对于已经实现的方法,如果我们想直接作为接口抽象方法的实现,我们还可以使用方法引用。
方法引用就是将一个已实现的方法,直接作为接口中抽象方法的实现(当然前提是方法定义得一样才行)
public interface Study {int sum(int a, int b); //待实现的求和方法
}
那么使用时候,可以直接使用Lambda表达式:
public static void main(String[] args) {Study study = (a, b) -> a + b;
}
只不过还能更简单,因为Integer类中默认提供了求两个int值之和的方法:
//Integer类中就已经有对应的实现了
public static int sum(int a, int b) {return a + b;
}
此时,我们可以直接将已有方法的实现作为接口的实现:
public static void main(String[] args) {Study study = (a, b) -> Integer.sum(a, b);//直接使用Integer为我们通过好的求和方法System.out.println(study.sum(10, 20));
}
我们发现,Integer.sum的参数和返回值,跟我们在Study中定义的完全一样,所以说我们可以直接使用方法引用:
public static void main(String[] args) {Study study = Integer::sum; //使用双冒号来进行方法引用,静态方法使用 类名::方法名 的形式System.out.println(study.sum(10, 20));
}
方法引用其实本质上就相当于将其他方法的实现,直接作为接口中抽象方法的实现。任何方法都可以通过方法引用作为实现:
public interface Study {String study();
}
如果是普通从成员方法,我们同样需要使用对象来进行方法引用:
public static void main(String[] args) {Main main = new Main();Study study = main::lbwnb; //成员方法因为需要具体对象使用,所以说只能使用 对象::方法名 的形式
}
public String lbwnb(){return "卡布奇诺今犹在,不见当年倒茶人。";
}
因为现在只需要一个String类型的返回值,由于String的构造方法在创建对象时也会得到一个String类型的结果,所以说:
public static void main(String[] args) {Study study = String::new; //没错,构造方法也可以被引用,使用new表示
}
反正只要是符合接口中方法的定义的,都可以直接进行方法引用,对于Lambda表达式和方法引用,在Java新特性中再来研究,这里就不多说了。
在理想的情况下,我们的程序会按照我们的思路去运行,按理说是不会出现问题的,但是,代码实际编写后并不一定是完美的,可能会有我们没有考虑到的情况,如果这些情况能够正常得到一个错误的结果还好,但是如果直接导致程序运行出现问题了呢?
public static void main(String[] args) {test(1, 0); //当b为0的时候,还能正常运行吗?
}
private static int test(int a, int b){return a/b; //没有任何的判断而是直接做计算
}
此时我们可以看到,出现了运算异常:
那么这个异常到底是什么样的一种存在呢?当程序运行出现我们没有考虑到的情况时,就有可能出现异常或是错误!
我们在之前其实已经接触过一些异常了,比如数组越界异常,空指针异常,算术异常等,他们其实都是异常类型,我们的每一个异常也是一个类,他们都继承自Exception
类!异常类型本质依然类的对象,但是异常类型支持在程序运行出现问题时抛出(也就是上面出现的红色报错)也可以提前声明,告知使用者需要处理可能会出现的异常!
异常的第一种类型是运行时异常,如上述的列子,在编译阶段无法感知代码是否会出现问题,只有在运行的时候才知道会不会出错(正常情况下是不会出错的),这样的异常称为运行时异常,异常也是由类定义的,所有的运行时异常都继承自RuntimeException
。
public static void main(String[] args) {Object object = String(); //这种情况就会出现运行时异常
}
又比如下面的这种情况:
public static void main(String[] args) {Object object = new Object();Main main = (Main) object;
}
异常的另一种类型是编译时异常,编译时异常明确指出可能会出现的异常,在编译阶段就需要进行处理(捕获异常)必须要考虑到出现异常的情况,如果不进行处理,将无法通过编译!默认继承自Exception
类的异常都是编译时异常。
protected native Object clone() throws CloneNotSupportedException;
比如Object类中定义的clone
方法,就明确指出了在运行的时候会出现的异常。
还有一种类型是错误,错误比异常更严重,异常就是不同寻常,但不一定会导致致命的问题,而错误是致命问题,一般出现错误可能JVM就无法继续正常运行了,比如OutOfMemoryError
就是内存溢出错误(内存占用已经超出限制,无法继续申请内存了)
public static void main(String[] args) {test();
}
private static void test(){test();
}
比如这样的一个无限递归的方法,会导致运行过程中无限制地向下调用方法,导致栈溢出:
这种情况就是错误了,已经严重到整个程序都无法正常运行了。又比如:
public static void main(String[] args) {Object[] objects = new Object[Integer.MAX_VALUE]; //这里申请一个超级大数组
}
实际上我们电脑的内存是有限的,不可能无限制地使用内存来存放变量,所以说如果内存不够用了,会直接:
此时没有更多的可用内存供我们的程序使用,那么程序也就没办法继续运行下去了,这同样是一个很严重的错误。
当然,我们这一块主要讨论的是异常。
异常其实就两大类,一个是编译时异常,一个是运行时异常,我们先来看编译时异常。
public class TestException extends Exception{public TestException(String message){super(message); //这里我们选择使用父类的带参构造,这个参数就是异常的原因}
}
编译时异常只需要继承Exception就行了,编译时异常的子类有很多很多,仅仅是SE中就有700多个。
异常多种多样,不同的异常对应着不同的情况,比如在类型转换时出错那么就是类型转换异常,如果是使用一个值为null的变量调用方法,那么就会出现空指针异常。
运行时异常只需要继承RuntimeException就行了:
public class TestException extends RuntimeException{public TestException(String message){super(message);}
}
RuntimeException继承自Exception,Exception继承自Throwable:
运行时异常同同样也有很多,只不过运行时异常和编译型异常在使用时有一些不同,我们会在后面的学习中慢慢认识。
当然还有一种类型是Error,它是所有错误的父类,同样是继承自Throwable的。
当别人调用我们的方法时,如果传入了错误的参数导致程序无法正常运行,这时我们就可以手动抛出一个异常来终止程序继续运行下去,同时告知上一级方法执行出现了问题:
public static int test(int a, int b) {if(b == 0)throw new RuntimeException("被除数不能为0"); //使用throw关键字来抛出异常return a / b;
}
异常的抛出同样需要创建一个异常对象出来,我们抛出异常实际上就是将这个异常对象抛出,异常对象携带了我们抛出异常时的一些信息,比如是因为什么原因导致的异常,在RuntimeException的构造方法中我们可以写入原因。
当出现异常时:
程序会终止,并且会打印栈追踪信息,因为各位小伙伴才初学,还不知道什么是栈,我们这里就简单介绍一下,实际上方法之间的调用是有层级关系的,而当异常发生时,方法调用的每一层都会在栈追踪信息中打印出来,比如这里有两个at
,实际上就是在告诉我们程序运行到哪个位置时出现的异常,位于最上面的就是发生异常的最核心位置,我们代码的第15行。
并且这里会打印出当前抛出的异常类型和我们刚刚自定义异常信息。
注意,如果我们在方法中抛出了一个非运行时异常,那么必须告知函数的调用方我们会抛出某个异常,函数调用方必须要对抛出的这个异常进行对应的处理才可以:
private static void test() throws Exception { //使用throws关键字告知调用方此方法会抛出哪些异常,请调用方处理好throw new Exception("我是编译时异常!");
}
注意,如果不同的分支条件会出现不同的异常,那么所有在方法中可能会抛出的异常都需要注明:
private static void test(int a) throws FileNotFoundException, ClassNotFoundException { //多个异常使用逗号隔开if(a == 1)throw new FileNotFoundException();else throw new ClassNotFoundException();
}
当然,并不是只有非运行时异常可以像这样明确指出,运行时异常也可以,只不过不强制要求:
private static void test(int a) throws RuntimeException {throw new RuntimeException();
}
至于如何处理明确抛出的异常,我们会下一个部分中进行讲解。
最后再提一下,我们在重写方法时,如果父类中的方法表明了会抛出某个异常,只要重写的内容中不会抛出对应的异常我们可以直接省去:
@Override
protected Object clone() {return new Object();
}
当程序没有按照我们理想的样子运行而出现异常时(默认会交给JVM来处理,JVM发现任何异常都会立即终止程序运行,并在控制台打印栈追踪信息)现在我们希望能够自己处理出现的问题,让程序继续运行下去,就需要对异常进行捕获,比如:
public static void main(String[] args) {try { //使用try-catch语句进行异常捕获Object object = String();} catch (NullPointerException e){ //因为异常本身也是一个对象,catch中实际上就是用一个局部变量去接收异常
}System.out.println("程序继续正常运行!");
}
我们可以将代码编写到try
语句块中,只要是在这个范围内发生的异常,都可以被捕获,使用catch
关键字对指定的异常进行捕获,这里我们捕获的是NullPointerException空指针异常:
可以看到,当我们捕获异常之后,程序可以继续正常运行,并不会像之前一样直接结束掉。
注意,catch中捕获的类型只能是Throwable的子类,也就是说要么是抛出的异常,要么是错误,不能是其他的任何类型。
我们可以在catch
语句块中对捕获到的异常进行处理:
public static void main(String[] args) {try {Object object = String();} catch (NullPointerException e){e.printStackTrace(); //打印栈追踪信息System.out.println("异常错误信息:"Message()); //获取异常的错误信息}System.out.println("程序继续正常运行!");
}
如果某个方法明确指出会抛出哪些异常,除非抛出的异常是一个运行时异常,否则我们必须要使用try-catch语句块进行异常的捕获,不然就无法通过编译:
public static void main(String[] args) {test(10); //必须要进行异常的捕获,否则报错
}
private static void test(int a) throws IOException { //明确会抛出IOExceptionthrow new IOException();
}
当然,如果我们确实不想在当前这个方法中进行处理,那么我们可以继续踢皮球,抛给上一级:
public static void main(String[] args) throws IOException { //继续编写throws往上一级抛test(10);
}
private static void test(int a) throws IOException {throw new IOException();
}
注意,如果已经是主方法了,那么就相当于到顶层了,此时发生异常再往上抛出的话,就会直接交给JVM进行处理,默认会让整个程序终止并打印栈追踪信息。
注意,如果我们要捕获的异常,是某个异常的父类,那么当发生这个异常时,同样可以捕获到:
public static void main(String[] args) throws IOException {try {int[] arr = new int[1];arr[1] = 100; //这里发生的是数组越界异常,它是运行时异常的子类} catch (RuntimeException e){ //使用运行时异常同样可以捕获到System.out.println("捕获到异常");}
}
当代码可能出现多种类型的异常时,我们希望能够分不同情况处理不同类型的异常,就可以使用多重异常捕获:
try {//....
} catch (NullPointerException e) {} catch (IndexOutOfBoundsException e){
} catch (RuntimeException e){}
但是要注意一下顺序:
try {//....
} catch (RuntimeException e){ //父类型在前,会将子类的也捕获
} catch (NullPointerException e) { //永远都不会被捕获
} catch (IndexOutOfBoundsException e){ //永远都不会被捕获
}
只不过这样写好像有点丑,我们也可以简写为:
try {//....
} catch (NullPointerException | IndexOutOfBoundsException e) { //用|隔开每种类型即可}
如果简写的话,那么发生这些异常的时候,都会采用统一的方式进行处理了。
最后,当我们希望,程序运行时,无论是否出现异常,都会在最后执行任务,可以交给finally
语句块来处理:
try {//....
}catch (Exception e){}finally {System.out.println("lbwnb"); //无论是否出现异常,都会在最后执行
}
try
语句块至少要配合catch
或finally
中的一个:
try {int a = 10;a /= 0;
} finally { //不捕获异常,程序会终止,但在最后依然会执行下面的内容System.out.println("lbwnb");
}
思考:try
、catch
和finally
执行顺序?
我们可以使用断言表达式来对某些东西进行判断,如果判断失败会抛出错误,只不过默认情况下没有开启断言,我们需要在虚拟机参数中手动开启一下:
开启断言之后,我们就可以开始使用了。
断言表达式需要使用到assert
关键字,如果assert后面的表达式判断结果为false,将抛出AssertionError错误。
public static void main(String[] args) {assert false;
}
比如我们可以判断变量的值,如果大于10就抛出错误:
public static void main(String[] args) {int a = 10;assert a > 10;
}
我们可以在表达式的后面添加错误信息:
public static void main(String[] args) {int a = 10;assert a > 10 : "我是自定义的错误信息";
}
这样就会显示到错误后面了:
断言表达式一般只用于测试,我们正常的程序中一般不会使用,这里只做了解就行了。
为了统计学生成绩,要求设计一个Score对象,包括课程名称、课程号、课程成绩,但是成绩分为两种,一种是以优秀、良好、合格
来作为结果,还有一种就是 60.0、75.5、92.5
这样的数字分数,可能高等数学这门课是以数字成绩进行结算,而计算机网络实验这门课是以等级进行结算,这两种分数类型都有可能出现,那么现在该如何去设计这样的一个Score类呢?
现在的问题就是,成绩可能是String
类型,也可能是Integer
类型,如何才能很好的去存可能出现的两种类型呢?
public class Score {String name;String id;Object value; //因为Object是所有类型的父类,因此既可以存放Integer也能存放String
public Score(String name, String id, Object value) {this.name = name;this.id = id;this.score = value;}
}
以上的方法虽然很好地解决了多种类型存储问题,但是Object类型在编译阶段并不具有良好的类型判断能力,很容易出现以下的情况:
public static void main(String[] args) {
Score score = new Score("数据结构与算法基础", "EP074512", "优秀"); //是String类型的
...
Integer number = (Integer) score.score; //获取成绩需要进行强制类型转换,虽然并不是一开始的类型,但是编译不会报错
}
使用Object类型作为引用,对于使用者来说,由于是Object类型,所以说并不能直接判断存储的类型到底是String还是Integer,取值只能进行强制类型转换,显然无法在编译期确定类型是否安全,项目中代码量非常之大,进行类型比较又会导致额外的开销和增加代码量,如果不经比较就很容易出现类型转换异常,代码的健壮性有所欠缺
所以说这种解决办法虽然可行,但并不是最好的方案。
为了解决以上问题,JDK 5新增了泛型,它能够在编译阶段就检查类型安全,大大提升开发效率。
泛型其实就一个待定类型,我们可以使用一个特殊的名字表示泛型,泛型在定义时并不明确是什么类型,而是需要到使用时才会确定对应的泛型类型。
我们可以将一个类定义为一个泛型类:
public class Score<T> { //泛型类需要使用<>,我们需要在里面添加1 - N个类型变量String name;String id;T value; //T会根据使用时提供的类型自动变成对应类型
public Score(String name, String id, T value) { //这里T可以是任何类型,但是一旦确定,那么就不能修改了this.name = name;this.id = id;this.value = value;}
}
我们来看看这是如何使用的:
public static void main(String[] args) {Score<String> score = new Score<String>("计算机网络", "EP074512", "优秀");//因为现在有了类型变量,在使用时同样需要跟上<>并在其中填写明确要使用的类型//这样我们就可以根据不同的类型进行选择了String value = score.value; //一旦类型明确,那么泛型就变成对应的类型了System.out.println(value);
}
泛型将数据类型的确定控制在了编译阶段,在编写代码的时候就能明确泛型的类型,如果类型不符合,将无法通过编译!因为是具体使用对象时才会明确具体类型,所以说静态方法中是不能用的:
只不过这里需要注意一下,我们在方法中使用待确定类型的变量时,因为此时并不明确具体是什么类型,那么默认会认为这个变量是一个Object类型的变量,因为无论具体类型是什么,一定是Object类的子类:
我们可以对其进行强制类型转换,但是实际上没多大必要:
public void test(T t){String str = (String) t; //都明确要用String了,那这里定义泛型不是多此一举吗
}
因为泛型本身就是对某些待定类型的简单处理,如果都明确要使用什么类型了,那大可不必使用泛型。还有,不能通过这个不确定的类型变量就去直接创建对象和对应的数组:
注意,具体类型不同的泛型类变量,不能使用不同的变量进行接收:
如果要让某个变量支持引用确定了任意类型的泛型,那么可以使用?
通配符:
public static void main(String[] args) {Test<?> test = new Test<Integer>();test = new Test<String>();Object o = test.value; //但是注意,如果使用通配符,那么由于类型不确定,所以说具体类型同样会变成Object
}
当然,泛型变量不止可以只有一个,如果需要使用多个的话,我们也可以定义多个:
public class Test<A, B, C> { //多个类型变量使用逗号隔开public A a;public B b;public C c;
}
那么在使用时,就需要将这三种类型都进行明确指定:
public static void main(String[] args) {Test<String, Integer, Character> test = new Test<>(); //使用钻石运算符可以省略其中的类型test.a = "lbwnb";test.b = 10;test.c = '淦';
}
是不是感觉好像还是挺简单的?只要是在类中,都可以使用类型变量:
public class Test<T>{private T value;
public void setValue(T value) {this.value = value;}
public T getValue() {return value;}
}
只不过,泛型只能确定为一个引用类型,基本类型是不支持的:
public class Test<T>{public T value;
}
如果要存放基本数据类型的值,我们只能使用对应的包装类:
public static void main(String[] args) {Test<Integer> test = new Test<>();
}
当然,如果是基本类型的数组,因为数组本身是引用类型,所以说是可以的:
public static void main(String[] args) {Test<int[]> test = new Test<>();
}
通过使用泛型,我们就可以将某些不明确的类型在具体使用时再明确。
不只是类,包括接口、抽象类,都是可以支持泛型的:
public interface Study<T> {T test();
}
当子类实现此接口时,我们可以选择在实现类明确泛型类型,或是继续使用此泛型让具体创建的对象来确定类型:
public class Main {public static void main(String[] args) {A a = new A();Integer i = a.test();}
static class A implements Study<Integer> { //在实现接口或是继承父类时,如果子类是一个普通类,那么可以直接明确对应类型@Overridepublic Integer test() {return null;}}
}
或者是继续摆烂,依然使用泛型:
public class Main {public static void main(String[] args) {A<String> a = new A<>();String i = a.test();}
static class A<T> implements Study<T> { //让子类继续为一个泛型类,那么可以不用明确@Overridepublic T test() {return null;}}
}
继承也是同样的:
static class A<T> {}
static class B extends A<String> {
}
当然,类型变量并不是只能在泛型类中才可以使用,我们也可以定义泛型方法。
当某个方法(无论是是静态方法还是成员方法)需要接受的参数类型并不确定时,我们也可以使用泛型来表示:
public class Main {public static void main(String[] args) {String str = test("Hello World!");}
private static <T> T test(T t){ //在返回值类型前添加<>并填写泛型变量表示这个是一个泛型方法return t;}
}
泛型方法会在使用时自动确定泛型类型,比如上我们定义的是类型T作为参数,同样的类型T作为返回值,实际传入的参数是一个字符串类型的值,那么T就会自动变成String类型,因此返回值也是String类型。
public static void main(String[] args) {String[] strings = new String[1];Main main = new Main();main.add(strings, "Hello");System.out.String(strings));
}
private <T> void add(T[] arr, T t){arr[0] = t;
}
实际上泛型方法在很多工具类中也有,比如说Arrays的排序方法:
Integer[] arr = {1, 4, 5, 2, 6, 3, 0, 7, 9, 8};
Arrays.sort(arr, new Comparator<Integer>() { //通过创建泛型接口的匿名内部类,来自定义排序规则,因为匿名内部类就是接口的实现类,所以说这里就明确了类型@Overridepublic int compare(Integer o1, Integer o2) { //这个方法会在执行排序时被调用(别人来调用我们的实现)return 0;}
});
比如现在我们想要让数据从大到小排列,我们就可以自定义:
public static void main(String[] args) {Integer[] arr = {1, 4, 5, 2, 6, 3, 0, 7, 9, 8};Arrays.sort(arr, new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) { //两个需要比较的数会在这里给出return o2 - o1; //compare方法要求返回一个int来表示两个数的大小关系,大于0表示大于,小于0表示小于//这里直接o2-o1就行,如果o2比o1大,那么肯定应该排在前面,所以说返回正数表示大于}});System.out.String(arr));
}
因为我们前面学习了Lambda表达式,像这种只有一个方法需要实现的接口,直接安排了:
public static void main(String[] args) {Integer[] arr = {1, 4, 5, 2, 6, 3, 0, 7, 9, 8};Arrays.sort(arr, (o1, o2) -> o2 - o1); //瞬间变一行,效果跟上面是一样的System.out.String(arr));
}
包括数组复制方法:
public static void main(String[] args) {String[] arr = {"AAA", "BBB", "CCC"};String[] newArr = pyOf(arr, 3); //这里传入的类型是什么,返回的类型就是什么,也是用到了泛型System.out.String(newArr));
}
因此,泛型实际上在很多情况下都能够极大地方便我们对于程序的代码设计。
现在有一个新的需求,现在没有String类型的成绩了,但是成绩依然可能是整数,也可能是小数,这时我们不希望用户将泛型指定为除数字类型外的其他类型,我们就需要使用到泛型的上界定义:
public class Score<T extends Number> { //设定类型参数上界,必须是Number或是Number的子类private final String name;private final String id;private final T value;
public Score(String name, String id, T value) {this.name = name;this.id = id;this.value = value;}
public T getValue() {return value;}
}
只需要在泛型变量的后面添加extends
关键字即可指定上界,使用时,具体类型只能是我们指定的上界类型或是上界类型的子类,不得是其他类型。否则一律报错:
实际上就像这样:
同样的,当我们在使用变量时,泛型通配符也支持泛型的界限:
public static void main(String[] args) {Score<? extends Integer> score = new Score<>("数据结构与算法", "EP074512", 60);
}
那么既然泛型有上界,那么有没有下界呢?肯定的啊:
只不过下界仅适用于通配符,对于类型变量来说是不支持的。下界限定就像这样:
那么限定了上界后,我们再来使用这个对象的泛型成员,会变成什么类型呢?
public static void main(String[] args) {Score<? extends Number> score = new Score<>("数据结构与算法基础", "EP074512", 10);Number o = Value(); //可以看到,此时虽然使用的是通配符,但是不再是Object类型,而是对应的上界
}
但是我们限定下界的话,因为还是有可能是Object,所以说依然是跟之前一样:public static void main(String[] args) {Score<? super Number> score = new Score<>("数据结构与算法基础", "EP074512", 10);Object o = Value();
}
通过给设定泛型上限,我们就可以更加灵活地控制泛型的具体类型范围。
前面我们已经了解如何使用泛型,那么泛型到底是如何实现的呢,程序编译之后的样子是什么样的?public abstract class A <T>{abstract T test(T t);
}
实际上在Java中并不是真的有泛型类型(为了兼容之前的Java版本)因为所有的对象都是属于一个普通的类型,一个泛型类型编译之后,实际上会直接使用默认的类型:
public abstract class A {abstract Object test(Object t); //默认就是Object
}
当然,如果我们给类型变量设定了上界,那么会从默认类型变成上界定义的类型:public abstract class A <T extends Number>{ //设定上界为Numberabstract T test(T t);
}
那么编译之后:
public abstract class A {abstract Number test(Number t); //上界Number,因为现在只可能出现Number的子类
}
因此,泛型其实仅仅是在编译阶段进行类型检查,当程序在运行时,并不会真的去检查对应类型,所以说哪怕是我们不去指定类型也可以直接使用:
public static void main(String[] args) {Test test = new Test(); //对于泛型类Test,不指定具体类型也是可以的,默认就是原始类型
}
只不过此时编译器会给出警告:
同样的,由于类型擦除,实际上我们在使用时,编译后的代码是进行了强制类型转换的:
public static void main(String[] args) {A<String> a = new B();String i = a.test("10"); //因为类型A只有返回值为原始类型Object的方法
}
实际上编译之后:
public static void main(String[] args) {A a = new B();String i = (String) a.test("10"); //依靠强制类型转换完成的
}
不过,我们思考一个问题,既然继承泛型类之后可以明确具体类型,那么为什么@Override
不会出现错误呢?我们前面说了,重写的条件是需要和父类的返回值类型和形参一致,而泛型默认的原始类型是Object类型,子类明确后变为其他类型,这显然不满足重写的条件,但是为什么依然能编译通过呢?
public class B extends A<String>{@OverrideString test(String s) {return null;}
}
我们来看看编译之后长啥样:
// Compiled from "B.java"
public st.entity.B st.entity.A<java.lang.String> {st.entity.B();java.lang.String test(java.lang.String);java.lang.Object test(java.lang.Object); //桥接方法,这才是真正重写的方法,但是使用时会调用上面的方法
}
通过反编译进行观察,实际上是编译器帮助我们生成了一个桥接方法用于支持重写:
public class B extends A {public Object test(Object obj) { //这才是重写的桥接方法st((Integer) obj); //桥接方法调用我们自己写的方法}public String test(String str) { //我们自己写的方法return null;}
}
类型擦除机制其实就是为了方便使用后面集合类(不然每次都要强制类型转换)同时为了向下兼容采取的方案。因此,泛型的使用会有一些限制:
首先,在进行类型判断时,不允许使用泛型,只能使用原始类型:
只能判断是不是原始类型,里面的具体类型是不支持的:
Test<String> test = new Test<>();
System.out.println(test instanceof Test);
//在进行类型判断时,不允许使用泛型,只能使用原始类型
还有,泛型类型是不支持创建参数化类型数组的:
要用只能用原始类型:
public static void main(String[] args) {Test[] test = new Test[10];
//同样是因为类型擦除导致的,运行时可不会去检查具体类型是什么
}
只不过只是把它当做泛型类型的数组还是可以用的:
学习了泛型,我们来介绍一下再JDK 1.8中新增的函数式接口。
函数式接口就是JDK1.8专门为我们提供好的用于Lambda表达式的接口,这些接口都可以直接使用Lambda表达式,非常方便,这里我们主要介绍一下四个主要的函数式接口:
Supplier供给型函数式接口:这个接口是专门用于供给使用的,其中只有一个get方法用于获取需要的对象。
@FunctionalInterface //函数式接口都会打上这样一个注解
public interface Supplier<T> {T get(); //实现此方法,实现供给功能
}
比如我们要实现一个专门供给Student对象Supplier,就可以使用:
public class Student {public void hello(){System.out.println("我是学生!");}
}
//专门供给Student对象的Supplier
private static final Supplier<Student> STUDENT_SUPPLIER = Student::new;
public static void main(String[] args) {Student student = ();student.hello();
}
Consumer消费型函数式接口:这个接口专门用于消费某个对象的。
@FunctionalInterface
public interface Consumer<T> {void accept(T t); //这个方法就是用于消费的,没有返回值
default Consumer<T> andThen(Consumer<? super T> after) { //这个方法便于我们连续使用此消费接口quireNonNull(after);return (T t) -> { accept(t); after.accept(t); };}
}
使用起来也是很简单的:
//专门消费Student对象的Consumer
private static final Consumer<Student> STUDENT_CONSUMER = student -> System.out.println(student+" 真好吃!");
public static void main(String[] args) {Student student = new Student();STUDENT_CONSUMER.accept(student);
}
当然,我们也可以使用andThen
方法继续调用:
这样,就可以在消费之后进行一些其他的处理了,使用很简洁的代码就可以实现:
public static void main(String[] args) {Student student = new Student();STUDENT_CONSUMER //我们可以提前将消费之后的操作以同样的方式预定好.andThen(stu -> System.out.println("我是吃完之后的操作!")) .andThen(stu -> System.out.println("好了好了,吃饱了!")).accept(student); //预定好之后,再执行
}
Function函数型函数式接口:这个接口消费一个对象,然后会向外供给一个对象(前两个的融合体)
@FunctionalInterface
public interface Function<T, R> {R apply(T t); //这里一共有两个类型参数,其中一个是接受的参数类型,还有一个是返回的结果类型
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {quireNonNull(before);return (V v) -> apply(before.apply(v));}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {quireNonNull(after);return (T t) -> after.apply(apply(t));}
static <T> Function<T, T> identity() {return t -> t;}
}
这个接口方法有点多,我们一个一个来看,首先还是最基本的apply
方法,这个是我们需要实现的:
//这里实现了一个简单的功能,将传入的int参数转换为字符串的形式
private static final Function<Integer, String> INTEGER_STRING_FUNCTION = Object::toString;
public static void main(String[] args) {String str = INTEGER_STRING_FUNCTION.apply(10);System.out.println(str);
}
我们可以使用compose
将指定函数式的结果作为当前函数式的实参:
public static void main(String[] args) {String str = INTEGER_STRING_FUNCTIONpose((String s) -> s.length()) //将此函数式的返回值作为当前实现的实参.apply("lbwnb"); //传入上面函数式需要的参数System.out.println(str);
}
相反的,andThen
可以将当前实现的返回值进行进一步的处理,得到其他类型的值:
public static void main(String[] args) {Boolean str = INTEGER_STRING_FUNCTION.andThen(String::isEmpty) //在执行完后,返回值作为参数执行andThen内的函数式,最后得到的结果就是最终的结果了.apply(10);System.out.println(str);
}
比较有趣的是,Function中还提供了一个将传入参数原样返回的实现:
public static void main(String[] args) {Function<String, String> function = Function.identity(); //原样返回System.out.println(function.apply("不会吧不会吧,不会有人听到现在还是懵逼的吧"));
}
Predicate断言型函数式接口:接收一个参数,然后进行自定义判断并返回一个boolean结果。
@FunctionalInterface
public interface Predicate<T> {boolean test(T t); //这个方法就是我们要实现的
default Predicate<T> and(Predicate<? super T> other) {quireNonNull(other);return (t) -> test(t) && st(t);}
default Predicate<T> negate() {return (t) -> !test(t);}
default Predicate<T> or(Predicate<? super T> other) {quireNonNull(other);return (t) -> test(t) || st(t);}
static <T> Predicate<T> isEqual(Object targetRef) {return (null == targetRef)? Objects::isNull: object -> targetRef.equals(object);}
}
我们可以来编写一个简单的例子:
public class Student {public int score;
}
private static final Predicate<Student> STUDENT_PREDICATE = student -> student.score >= 60;
public static void main(String[] args) {Student student = new Student();student.score = 80;if(st(student)) { //test方法的返回值是一个boolean结果System.out.println("及格了,真不错,今晚奖励自己一次");} else {System.out.println("不是,Java都考不及格?隔壁初中生都在打ACM了");}
}
我们也可以使用组合条件判断:
public static void main(String[] args) {Student student = new Student();student.score = 80;boolean b = STUDENT_PREDICATE.and(stu -> stu.score > 90) //需要同时满足这里的条件,才能返回st(student);if(!b) System.out.println("Java到现在都没考到90分?你的室友都拿国家奖学金了");
}
同样的,这个类型提供了一个对应的实现,用于判断两个对象是否相等:
public static void main(String[] args) {Predicate<String> predicate = Predicate.isEqual("Hello World"); //这里传入的对象会和之后的进行比较System.out.st("Hello World"));
}
通过使用这四个核心的函数式接口,我们就可以使得代码更加简洁,具体的使用场景会在后面讲解。
Java8还新增了一个非常重要的判空包装类Optional,这个类可以很有效的处理空指针问题。比如对于下面这样一个很简单的方法:private static void test(String str){ //传入字符串,如果不是空串,那么就打印长度if(!str.isEmpty()) {System.out.println("字符串长度为:"+str.length());}
}
但是如果我们在传入参数时,丢个null进去,直接原地爆炸:
public static void main(String[] args) {test(null);
}
private static void test(String str){ if(!str.isEmpty()) { //此时传入的值为null,调用方法马上得到空指针异常System.out.println("字符串长度为:"+str.length());}
}
因此我们还需要在使用之前进行判空操作:
private static void test(String str){if(str == null) return; //这样就可以防止null导致的异常了if(!str.isEmpty()) {System.out.println("字符串长度为:"+str.length());}
}
虽然这种方式很好,但是在Java8之后,有了Optional类,它可以更加优雅地处理这种问题,我们来看看如何使用:
private static void test(String str){Optional.ofNullable(str) //将传入的对象包装进Optional中.ifPresent(s -> System.out.println("字符串长度为:"+s.length())); //如果不为空,则执行这里的Consumer实现
}
优雅,真是太优雅了,同样的功能,现在我们只需要两行就搞定了,而且代码相当简洁。如果你学习过JavaScript或是Kotlin等语言,它的语法就像是:
var str : String? = null
str?.upperCase()
并且,包装之后,我们再获取时可以优雅地处理为空的情况:
private static void test(String str){String s = Optional.ofNullable(str).get(); //get方法可以获取被包装的对象引用,但是如果为空的话,会抛出异常System.out.println(s);
}
我们可以对于这种有可能为空的情况进行处理,如果为空,那么就返回另一个备选方案:
private static void test(String str){String s = Optional.ofNullable(str).orElse("我是为null的情况备选方案");System.out.println(s);
}
是不是感觉很方便?我们还可以将包装的类型直接转换为另一种类型:
private static void test(String str){Integer i = Optional.ofNullable(str).map(String::length) //使用map来进行映射,将当前类型转换为其他类型,或者是进行处理.orElse(-1);System.out.println(i);
}
当然,Optional的方法比较多,这里就不一一介绍了。
本文发布于:2024-02-01 21:01:16,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170679247639395.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |