文章目录
- 前言
- 一、包装类
- 二、数组 -- int[]、String[]...
- 三、Arrays:操作数组的工具类(极其常用)--- 注意下面常用方法均支持泛型
- 1 public static String toString(数组):把数组拼接成一个字符串(和python里面打印列表效果一样)
- 2 public static int binarySearch(数组):二分法查找元素返回对应(索引)或(-插入点-1)
- 3 public static int[] copyOf(原数组,新数组长度):拷贝数组
- 4 public static int[] copyOfRange(原数组,起始索引,结束索引):拷贝数组(指定范围)
- 5 public static void fill(数组,元素):填充数组
- 6 public static void sort(数组):按照默认方式进行数组排序
- 7 public static void sort(数组,排序规则):按照排序规则进行数组排序
- 四、Java函数式编程 = lambda表达式 + 接口(特别是一个方法的接口)
- 五、集合:ArryList --- 数组列表
- 1 ArryList对象的创建
- 2 ArryList常见成员方法
- (1)boolean add(E e) : 添加元素,返回值表示是否添加成功
- (2)void add(int index, E e) :在指定索引位置插入元素。
- (3)boolean remove(E e) : 删除第一个指定元素 e,返回值表示是否删除成功
- (4)E remove(int index) : 删除指定索引元素,返回被删除元素
- (5)E set(int index, E e) : 修改指定索引下的元素,返回原来的元素
- (6)E get(int index) : 获取指定索引处的元素
- (7)int size() : 返回集合的长度
- (8)boolean isEmpty() :判断数组列表是否为空。
- 3 ArryList的遍历
- 五、泛型
前言
本节会总结Java中各种数据结构里面的使用方法,包括之前的学过的数组、ArrayList、还包括链表、哈希表、树等数据结构一起其中的高级实用API。本博客会详细记录各种用法,作为个人的查询文档。
在学习这些数据结构前,前面有必要先来好好学习一下包装类,由于Java基本数据类型(可变)大部分集合都是不允许放进去的,我们必须要放其对应的包装类(不可变)才被允许放进去,所以学习这些集合第一关就是对应的包装类。
这里是上部分,主要是 数组、和ArrayList这两个数据结构一起其对应的一些工具和辅助知识,有了这些辅助知识再看其他集合类数据结构就简单了。辅助知识包括:包装类、Arrays、泛型、lambda表达式、等等这些不可忽视的。
一、包装类
在前面我们其实简单讲过包装类,但讲的比较简单,这里做一个详细的笔记,方便后续查阅。
- 包装类:基本数据类型对应的引用数据累加
将Java中的基本数据类型重新写成引用数据类型,并且是不可变的;这样就和python一样了,Python里面有一句名言,万物皆对象,有了包装类这句话是不是也可以移到Java中,Java中万物皆对象。 - 并且Java的集合的高级数据结构里面,基本数据类型由于其可变性,是不允许存进集合的;所以我们就需要将不可变的包装类存进集合实现相同的效果。
给出Java中基本数据类型和其对象的包装类内存图就清楚二者的关系了:
基本数据类型 | 对应的包装类 |
---|---|
byte | Byte |
short | Short |
char | Character |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
可以看到只有char和int的包装类名有点变化要单独记一下,其余的都是首字母大写就可以了。
具体怎么用的建议先了解ArryList,下面给出示例:包装类就可以添加进集合了,表示方法和普通的写法一样,只是泛型要写包装类就是了。
下面以Integer演示包装类的用法,其他的都类似
1、Integer
(1)基本用法
// 创建 Integer
Integer a = 10;
Integer b = 11;
// 进行运算
Integer sum = a + b;
System.out.println(sum); // 21
Double d = 5.0;
Double v = a * d;
double v1 = a * d; // 这样也可以
System.out.println(v); // 50.0 隐式类型转换这些都还有
System.out.println(v1); // 50.0
可以看到和基本数据类型完全一样的用法,运算也一样,并且包装类和其对于的基本数据类型之间还存在自动隐式转换,使得二者是互通的。(这里设计到了我们等下要将的自动装箱、自动拆箱机制)
再看这个ArrayList list = new ArrayList<>();的例子ArrayList只能包装类进(可以跳到自动装箱哪里)
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 自动装箱
list.add(2);
list.add(3);
for (Integer i : list) {
System.out.print(i + " "); // 1 2 3
这就是典型的自动装箱机制。
(2)JDK5前的包装类用法(了解即可,能更好帮助我们理解下面的自动装箱和自动拆箱机制)
在JDK5以前创建一个Integer对象,要用到一下这些复杂的方法(部分现在都已经废弃了):
【注】:下面代码高版本jdk运行不了,方法已经废弃,这里只是做一个示例
Integer i1 = new Integer(1);
Integer i2 = new Integer(2);
// 如果要用上面JDK5前的这些方法创建Integer对象,进行计算需要手动拆箱,在装箱
// 因为对象之间不能直接进行运算
// 步骤:
// 1. 先将对象转换为基本数据类型(拆箱)
// 2. 进行运算
// 3. 将基本数据类型转换为对象(装箱)
int result = i1.intValue() + i2.intValue(); // 拆箱并运算
Integer i3 = new Integer(result); // 装箱
System.out.println(i3);
可以看到,包装类进行运算如果要手动拆箱后手动装箱,太麻烦了。没错,大佬们也觉得太麻烦了,于是JDK5以后就有了自动拆箱和自动装箱机制。
一个面试问题:
Integer i1 = Integer.valueOf(127);
Integer i2 = Integer.valueOf(127);
System.out.println(i1 == i2); // true
Integer i3 = Integer.valueOf(128);
Integer i4 = Integer.valueOf(128);
System.out.println(i3 == i4); // false
// 下面new出来的好理解,只要new出来的对象地址不同,肯定是false
Integer i5 = new Integer(127);
Integer i6 = new Integer(127);
System.out.println(i5 == i6); // false
Integer i7 = new Integer(128);
Integer i8 = new Integer(128);
System.out.println(i7 == i8); // false
关键在于前两段,为什么127的是true , 128的是false
查看源码会发现 -128到127间(闭区间)的因为在实际开发中应用的比较多,如果每次使用都new对象浪费内存,于是底层这样设计为:
- 提前把这个[-128,127]范围之内的每一个数据都创建好对象放进一个数组存起来
- 如果要用到了不会创建新的,而是返回已经创建好的对象,所以127地址一样的是,128地址不一样。
这是一个面试题,所以我也在这里写一下。
(3)自动装箱与自动拆箱机制 — 导致:int和Integer,包装类和对应的基本数据类型在不需要进集合的情况下是互通的(重要重要!!!!!)
Integer i1 = 100; // 自动装箱
Integer i2 = 100;
Integer sum = i1 + i2; // 内部会自动拆箱,然后再装箱
System.out.println(sum);
int i3 = 99;
int sum2 = i1 + i3; // i1会自动拆箱
System.out.println(sum2); // 199
// 注:除了部分集合只能包装类进,其他情况由于自动装箱拆箱,基本类型和包装类可以互相转换的,底层自动实现
再看这个ArrayList list = new ArrayList<>();的例子ArrayList只能包装类进
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 自动装箱
list.add(2);
list.add(3);
for (Integer i : list) {
System.out.print(i + " "); // 1 2 3
这就是典型的自动装箱机制。
(4)Integer的常用方法
— 进制转换方法
方法名 | 说明 |
---|---|
public static String toBinaryString(int i) | 得到二进制 |
public static String toOctalString(int i) | 得到八进制 |
public static String toHexString(int i) | 得到十六进制 |
// 1 把整数转换成二进制
String str1 = Integer.toBinaryString(100);
System.out.println(str1); // 1100100
// 2 把整数转换成八进制
String str2 = Integer.toOctalString(100);
System.out.println(str2); // 144
// 3 把整数转换成十六进制
String str3 = Integer.toHexString(100);
System.out.println(str3); // 64
2 包装类实现类型转换(重要!!!)
(1)Integer:public static int parseInt(String s):将字符串类型的整数转换成int类型的整数
在python中这个功能 int()就能转,但在Java中必须这样才行
int i = Integer.parseInt("123"); // 字符串转整数 (自动拆箱)
System.out.println(i); // 123
System.out.println(i + 1); // 124
// 细节:"123"里面必须是数字,里面有字母无法转,会报错
【注】:除了Character包装类,其他7中包装类都有其对应的paseXxx的方法进行类型转换
有了这个就可以将之前的键盘录入代码做一个规范了 包装类数据类型转换在键盘录入中的应用
(2)Boolean:public static boolean parseBoolean(String s):将字符串类型的布尔转换成boolean
String str = "true";
boolean b = Boolean.parseBoolean(str); // 字符串转布尔
System.out.println(b); // true
二、数组 – int[]、String[]…
【注】:java的数组和Python的列表有很大的不同之处,下面的两个不同需要特别注意一下。
- (1)java的数组里面也可以是任意数据类型、如整数、浮点数、字符、字符串等或者对象(如类的实例),但需要注意的是,一个数组里面只能含有一种数据类型,也就是说java数组里面只能存在同一种数据类型,而Python的一个列表内却是可以同时含有多种不同数据类型,这是一个主要的小区别。
- (2)数组长度不可变:一旦数组创建并分配了空间,其长度不可改变。这点也是和Python不同,因此,如果需要动态增删元素,建议使用ArrayList等其他动态数组类。
- (3)java中数组这些没有切片操作这些。
1 数组的创建与初始化
(1)静态初始化
-
初始化就是在内存中为数组容器开辟空间,并将数据存入容器中的过程
-
注:数组一旦创建过后其长度就固定了,不可改变
数组的长度直接访问数组的length属性即可:arr.length
其中语法有下面完整写法和简单写法两种
- 简单格式:数组类型[] 数组名 = {元素1,元素2,元素3…}
- 完整格式:数组类型[] 数组名 = new 数组类型[]{元素1,元素2,元素3…}
public class Business {
public static void main(String[] args) {
int[] arr1 = new int[]{1,2,3}; // 这种是复杂的写法,一般使用下面的简单写法即可
int[] arr2 = {1,2,3};
String[] arr3 = new String[]{"a","b","c"};
String[] arr4 = {"a","b","c"};
System.out.println(arr2); // [I@4eec7777
System.out.println(arr4); // [Ljava.lang.String;@3b07d329
}
}
上面有个小细节,在java中直接打印数组打印的是数组的地址,下面对java中的地址格式做一个解释说明。
java地址格式说明:[I@4eec7777上面的这个地址,[ 表示是数组,I表示里面数据类型是整数 。@没有固定含义,就是一个固定格式,4eec7777这个才是真正的地址值。
(2)动态初始化
动态初始化:初始化时只指定数组长度,由系统为数组分配初始值。
语法: 数据类型[] 数组名 = new 数据类型[数组长度]
【注】:和静态相比,右边大括号没有了,中括号里面变数组长度了,要注意
int[] arr = new int[100]
数组默认的初始化规律:
- 整数类型:默认初始化为0
- 小数类型:默认初始化为0.0
- 字符类型:默认初始化为‘/u0000’ 其实就是空格
- 布尔类型:默认初始化为false
- 引用数据类型:默认初始化为 null
2 数组元素访问与修改
语法:
- 访问: 数组名[索引]
- 修改:数组名[索引] = 具体数据/变量
【注】:
1、java中的索引也是从0开始的
2、java中数组索引不能是负数,这和Python不一样
String[] arr4 = {"a","b","c"};
String s = arr4[0];
System.out.println(s); // a
arr4[1] = "哈哈";
System.out.println(arr4[1]); // 哈哈
3 数组的遍历
关于数组的变量java提供了一种增强的增强型 for 循环 (foreach)
基本语法:for (int i : arr)
以下面案例为例:定义一个数组[1,2,3,4,5],并用遍历数组求里面元素的和。
public class Business {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int sum = 0;
for (int i:arr){
sum += i;
}
System.out.println(sum);
}
}
另外还有一种普通for循环结合数组长度来的就没有那么方便了
public class Business {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
System.out.println(sum);
}
}
【注】:今后我们尽量使用增强型的和Python类似
4 数组的内存图(重要!!!!!!!)
由于数组前面已经有博客记录过了,这里直接给出衔接数组
5 二维数组
数组里面存数组就是二维数组了。
(1)静态初始化
- 语法格式: 数据类型[][] 数组名 = new 数据类型[][]{{元素1,元素2},{元素1,元素2}}
- 简化格式:数据类型[][] 数组名 = {{元素1,元素2},{元素1,元素2}}
- 范例:int[][] arr = new int[][]{{1,2},{3.4}} 或者 int[][] arr = {{1,2},{3.4}}
- 注意:二维数组数据类型指定了,所有数据类型都要是指定的数据类型,就算是内部的数组里面的元素也应该是最先规定的数据类型。
public class Business {
public static void main(String[] args) {
// int[][] arr = new int[][]{{1,2,3},{4,5,6,7,8}};
int[][] arr = {{1,2,3},{4,5,6,7,8}};
//数组访问
System.out.println(arr[0][0]); // 1
// 二维数组的遍历,也可以使用增强for循环
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j]+" ");
}
}
System.out.println();
// 二维数组的遍历,增强for循环
for (int[] a : arr) {
for (int i : a) {
System.out.print(i+" ");
}
}
}
}
(2)动态初始化
- 语法格式:数据类型[][] 数组名 = new 数据类型[m][n]
其中,m,n理解成m行n列就可以了。所以没有静态初始化那么灵活可以内部数组长度不一样,不为二维数组不就是为了处理矩阵问题,够用就行。如果要动态初始化内部数组长度不同的怎么办,也有办法,由于并不常用,就放在下面的内存图中顺便作为介绍内存图的案例了。 - 范例: int[][] arr = new int[2][3];
public class Business {
public static void main(String[] args) {
// int[][] arr = new int[][]{{1,2,3},{4,5,6,7,8}};
int[][] arr = new int[2][3];
arr[0][0] = 100;
// 遍历
for (int[] row : arr){
for (int data : row){
System.out.print(data + " ");
}
System.out.println();
}
}
}
/* 输出:
100 0 0
0 0 0
*/
(3)二维数组的内存图(重要重要重要!!!!!!!!!!!)
由于数组前面已经有博客记录过了,这里直接给出衔接数组
6 数组小练习
(1)求最值
public class Business {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int max = arr[0]; // 初始化最大值为数组第一个元素,一定要是数组中的元素,不能是0
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
System.out.println("Max: " + max);
}
}
(2)按索引交换数组对应元素
在Python中交换两个变量的值直接 a,b = b,a即可,但是java不行,中间必须用一个中间变量过度。
int a = 10;
int b = 5;
int tem = a;
a = b;
b = tem;
System.out.println("a = " + a); // 5
System.out.println("b = " + b); // 10
知道上面那个,数组内部元素交换就按上面技巧来就是了。
其实还有一些其他花里胡哨的骚套路可以节省tem的内存消耗。
看下面这个题目:
思路:用双指针,while(i!= j)
public class Business {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
int i = 0;
int j = arr.length - 1;
while(i!=j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;
j--;
}
for(int k=0; k<arr.length; k++){
System.out.println(arr[k]);
}
}
}
三、Arrays:操作数组的工具类(极其常用)— 注意下面常用方法均支持泛型
有没有发现数组里面没有什么方法吗?只有一个arr.length访问长度属性的属性访问手段。不要担心,这里就有这么一个操作数组的工具类,里面提供大量的静态方法操作数组,所以这个工具类用到的场景极其广泛。
下面同样给出常用的操作方法
【注】:下述方法部分有其对应的泛型形式,为了方便就只给出了int的,但是要知道有可能是泛型,像拷贝数组的那几个方法由于是泛型,所以里面是对象也是能拷贝的
1 public static String toString(数组):把数组拼接成一个字符串(和python里面打印列表效果一样)
int[] arr = {1,2,3,4,5};
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5]
2 public static int binarySearch(数组):二分法查找元素返回对应(索引)或(-插入点-1)
这个方法有一些注意事项:
- (1)由于是二分查找,所以要求数组有序且小到大排列
- (2)如果能找到对应元素,返回对应索引值;如果没有找到,返回 -插入点-1
因此该方法不光可以查找元素,还可以返回插入点喔 - (3)支持泛型
int[] arr = {1,2,3,4,5,6,7,8,9,10};
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
System.out.println("-----------binarySearch()-----------");
// binarySearch:二分查找法查找元素
// 细节1:二分查找的前提:数组中的元素必须是有序,数组中的元素必须是升序的
// 细节2:如果要查找的元素是存在的,那么返回的是真实的索引
// 但是,如果要查找的元素是不存在的,返回的是–插入点- 1
// 疑问:为什么要减1呢?
// 解释:如果此时,我现在要查找数字0,那么如果返回的值是-插入点,就会出现问题了。
// 如果要查找数字0,此时0是不存在的,但是按照上面的规则-插入点,应该就是-0
// 为了避免这样的情况,Java在这个基础上又减一。
System.out.println(Arrays.binarySearch(arr,10)); // 9
System.out.println(Arrays.binarySearch(arr,2)); // 1
System.out.println(Arrays.binarySearch(arr,20)); // -11
3 public static int[] copyOf(原数组,新数组长度):拷贝数组
【注】:
(1)该方法底层其实是调用的System里面的public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) — 拷贝数组方法 参考博客;该方法对引用数据类型是浅拷贝(拷贝地址)
(2)支持泛型
int[] arr = {1,2,3,4,5,6,7,8,9,10};
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// copyof:拷贝数组
// 参数一:老数组
// 参数二:新数组的长度
// 方法的底层会根据第二个参数来创建新的数组
// 如果新数组的长度是小于老数组的长度,会部分拷贝
// 如果新数组的长度是等于老数组的长度,会完全拷贝
// 如果新数组的长度是大于老数组的长度,会补上默认初始值
int[] newArr1 = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(newArr1)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
int[] newArr2 = Arrays.copyOf(arr, 5);
System.out.println(Arrays.toString(newArr2)); // [1, 2, 3, 4, 5]
int[] newArr3 = Arrays.copyOf(arr, 15);
System.out.println(Arrays.toString(newArr3)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0]
该方法API文档上有一个设计成泛型方法,所以能拷贝对象
public static <T> T[] copyOf(T[] original, int newLength)
public class Test {
public static void main(String[] args){
Student s1 = new Student(12,"张三");
Student s2 = new Student(11,"李三");
Student[] arr = {s1,s2};
Student[] copy = Arrays.copyOf(arr, arr.length);
System.out.println(Arrays.toString(copy));
// [cn.hjblogs.demo.Student@404b9385, cn.hjblogs.demo.Student@6d311334]
}
}
class Student{
String name;
int age;
int[] arr = new int[3];
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
}
4 public static int[] copyOfRange(原数组,起始索引,结束索引):拷贝数组(指定范围)
【注】:
(1)该方法底层其实是调用的System里面的public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) — 拷贝数组方法 参考博客;该方法对引用数据类型是浅拷贝(拷贝地址)
(2)支持泛型
int[] arr = {1,2,3,4,5,6,7,8,9,10};
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// copyOfRange():拷贝数组,指定范围(左闭右开)
int[] newArr = Arrays.copyOfRange(arr, 0, 9);
System.out.println(Arrays.toString(newArr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
5 public static void fill(数组,元素):填充数组
int[] arr = {1,2,3,4,5,6,7,8,9,10};
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// fill:填充数组
Arrays.fill(arr, 0);
System.out.println(Arrays.toString(arr)); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Arrays.fill(arr, 2, 5, 100);
System.out.println(Arrays.toString(arr)); // [0, 0, 100, 100, 100, 0, 0, 0, 0, 0]
可以看到还可以指定起始和结束索引填充,看下底层,和你想的一模一样,你来写你也会这么写。Java学习一个方法一定不能只学这一个方法,立马就要类比他的重载方法,泛型这些。
6 public static void sort(数组):按照默认方式进行数组排序
// sort排序,默认情况下是升序
int[] arr = {10,4,3,7,8,9,1,2,5,6};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
7 public static void sort(数组,排序规则):按照排序规则进行数组排序
- 排序规则:这里的排序规则源码里面是一个接口,前面学习接口的时候讲过,如果一个方法的参数是接口,那么传入的就应该是一个该接口的实现类,这里一定兕想到匿名内部类。其实除了传入匿名内部类,还有另一种方式,传入一个lambda表达式
(1)传入一个接口的实现类
(2)传入一个lambda表达式
lambda表达式下一节会讲,这里就先讲一下传入匿名内部类的方式是怎么做的。
其实这个传入匿名内部类的逻辑很简单,方法里面传入了一个实现类对象,方法内调用实现类里面的某个实现方法就是了。
好了,我们来看一下,sort函数的基本用法再来慢慢解释:
规则:(其实记住这个规则就会用了)
- o1 - o2 升序 : 从小到大
- o2 - o1 降序 : 从大到小
Integer[] arr = {2,3,1,5,6,7,8,4,9};
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
// o1 - o2 升序 : 从小到大
// o2 - o1 降序 : 从大到小
}
});
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
解释一下:
(1)new 出来的就是那个接口(接口记不住,用到查就是了)的一个匿名内部类,里面有一个compare方法就是排序规则,排序过程中内部就是不断调这个对象里面的这个方法进行排序。
(2)这个排序方法底层的使用插入排序+二分查找的排序原理,我们当成插入排序来理解就可以了。底层第一轮就讲第一个元素当有序,其余都是无序,从第二个元素就不断往左插(这里要先学一下插入排序的算法原理)
(3)o1(要插入的元素) :表示无序序列中,遍历到的每一个元素
(4)o2 : 表示有序序列中元素
返回值(比较规则) :
负数: 表示当前要插入的元素o1放在前面
正数: 表示当前要插入的元素o1放在后面
0:表示当前要插入的元素跟现在的元素比是一样的们也会
o1 - o2>0, o1放后面,就是升序;
o2 - o1<0, o1放前面,就是降序序
看完上面我写的比较规则就理解了,但这个调用太麻烦了,下面我们会学习一种简单点的调用方式lambda表达式、
可以结合python的sort排序函数一起看效果更好python sort函数
四、Java函数式编程 = lambda表达式 + 接口(特别是一个方法的接口)
1 Java函数式编程 = lambda表达式 + 函数式接口
其实如果结合python的sort函数来看python sort函数python中的规则是直接传入一个函数或者是匿名函数(lambda表达式)+ 一个reverse参数来确定的。简单来说传入一个函数就搞定了。
再结合Java这里的传入一个接口的实现类,好像思路是一样的。Java里面无法传入函数,但是接口可以模拟函数,接口本身在Java中就能起到一种函数的作用,只是其使用必须通过实现类,只不过是语法复杂一些,idea是互通的。是不是对Java里面接口的功能有了更加深入的理解:
- (1)定义契约:顶层协议接口(减少业务对接)
- (2)支持函数式编程:近似代替函数的功能,匿名内部类(对象)就是典型的例子
在Java 8引入lambda表达式后,接口(特别是只有一个抽象方法的接口,即“函数式接口”)成为了Java函数式编程的基础。
要使用Lambda表达式,通常需要使用函数式接口。函数式接口是一个只包含一个抽象方法的接口 - (3)函数式接口:有且只有一个抽象方法的接口叫做函数式接口,接口上方用@FunctionalInterface注解
接下来我们就是要学习这种Java里面的函数式编程
首先我们要理解什么是Java中的lambda表达式,就要理解什么是匿名(内部)类(实际上是一个实例化对象,接口的一个实现类对象),知道什么是匿名内部类,那么lambda你应该也能够猜到是什么东西了(结合上面的sort方法的匿名内部类调用示例)。
-
没错,Lambda = 只有一个方法接口的匿名内部类,因此也是一个对象。结合前面学过的接口的多态的知识接口的多态,就可以理解下面具体的示例是什么了。
-
lambda表达式语法:
我们应该发现了,Java的lambda表达式可以没有返回值,还有方法体,这和我们python里面的函数一样。另外python中的lambda必须返回一个结果 lambda 参数:表达式 ,表达式就是返回的结果。二者区别还是很大的,Java的lambda表达式更像我们Python中一般的函数。
有了,这个,我们就可以模拟函数式编程功能了:
示例:使用Lambda表达式和函数式接口
// 定义一个函数式接口
@FunctionalInterface
interface GreetingService {
void sayMessage(String message);
}
public class LambdaExample {
public static void main(String[] args) {
// 使用Lambda表达式实现接口
GreetingService greetService1 = message -> System.out.println("Hello, " + message);
// 调用接口方法
greetService1.sayMessage("World");
greetService1.sayMessage("Lambda");
}
}
解释:
(1)定义函数式接口:GreetingService接口是一个典型的函数式接口,它只有一个抽象方法sayMessage。@FunctionalInterface注解用来表明这个接口是一个函数式接口。如果你试图在这个接口中添加第二个抽象方法,编译器会报错。
(2)GreetingService greetService1 = message -> System.out.println("Hello, " + message);
左边,是接口的多态使用
右边,在这里,我们使用Lambda表达式来实现GreetingService接口的sayMessage方法。message -> System.out.println("Hello, " + message);是一个Lambda表达式=GreetingService接口的匿名内部类(对象),它接收一个message参数,并输出一个字符串。
左边是接口的多态接口变量greetService1 接住message -> System.out.println("Hello, " + message);这个实现类对象。
这样理解逻辑一下就清晰了。
2 lambda的自动推导机制和省略机制(能自动推导的都能删)
(1)根据其在方法里面参数的位置,lambda表达式可以自动推导出自己是哪一个接口的实现类对象(毕竟语法格式上面没有接口名字信息,只能通过所在方法里面位置来猜测)
(2)关于实现接口里面抽象方法参数名必须不能变(建议先new出匿名类后删:这样就知道接口里面方法参数名叫什么了),参数前面数据类型可以删。
(3)如果方法体只有一行代码,{ }可以删并且{}里面的 ; 也要删,全写一行即可。(这个大可不必,留着,不然极有可能你要重新格式化代码)
public class Test {
public static void main(String[] args) {
// 1 传入匿名内部类对象
method(new Add() {
@Override
public void adding(int a, int b) {
System.out.println(a + b);
}
}, 10, 20); // 30
// 2 传入Lambda表达式
// 根据所在方法的的参数位置,自动推断出自己是哪个接口的实现类对象
// 接口的抽象方法参数名不可以改变,但是其数据类型可以省略
method((a, b) -> {
System.out.println(a + b);
}, 10, 20); // 30
}
// 1 调用一个方法时,如果方法的形参是一个接口类型,那么我们要传递这个接口的实现类对象,
// 一般最常见的如果实现类对象只要用到一次,一般是是传递匿名内部类对象或Lambda表达式(lambda表达式必须要求函数式接口)
public static void method(Add add, int a, int b) {
add.adding(a, b);
}
@FunctionalInterface
interface Add {
public abstract void adding(int a, int b);
}
}
3 sort排序lambda表达式改进
Integer[] arr = {2,3,1,5,6,7,8,4,9};
// 根据参数位置自动推断是那个接口的实现类对象
Arrays.sort(arr, (o1, o2) -> o2 - o1);
// Arrays.sort(arr,(Integer o1, Integer o2)->{
// return o2 - o1;
// });
System.out.println(Arrays.toString(arr)); // [9, 8, 7, 6, 5, 4, 3, 2, 1]
五、集合:ArryList — 数组列表
查阅帮助文档可以发现这个类在 java.util包下面
在之前创建数组我们采用的构造方式是 数组类型[] 数组名 = new 数组类型[]{元素1,元素2,元素3…}这种方式,这是独属于数组的构造方式。今后我们大多集合(还有些其他集合使用其他构造方式)都是使用 new + 泛型这种构造方式来创建集合对象。什么是泛型,打开ArryList的帮助文档:
上面这个<数据类型>括号就是泛型,用来指定集合里面的数据类型的。
了解了泛型就可以往下学习了
- ArryList基本数据类型不能进,必须是对应的包装类才行
- ArryList的长度是可以变的,理解成一个长度可变的动态数组
- ArryList提供了一个subList(int fromIndex, int toIndex)方法可以达到类似切片的效果 — 有兴趣可以查看API帮助文档(具体是浅复制还是深复制还得查阅资料),由于重点不在切片,所以这里就不详细说明,用到查资料即可。
- 我们也可以不指定泛型,JDK5前没有泛型就是这么用的。不指定泛型,所以进集合ArryList的元素类型都是总父类Object,无论你是字符串对象还是包装类的什么,声明的变量类型都是Object
1 ArryList对象的创建
- jdk7前的语法: Array<数据类型> 变量名 = new Array<数据类型> ()
- jdk7以后的语法: Array<数据类型> 变量名 = new Array<> ()
// ArrayList<String> ls = new ArrayList<String>();
ArrayList<String> ls = new ArrayList<>();
ls.add("a");
ls.add("b");
System.out.println(ls); // [a, b]
我们也可以不指定泛型,JDK5前没有泛型就是这么用的。不指定泛型,所以进集合ArryList的元素类型都是总父类Object,无论你是字符串对象还是包装类的什么,声明的变量类型都是Object。但很遗憾,虽然这样看起来和python列表很类似,但在Java里面有巨大风险,所以在JDK5以后由于Java也不想动源码,所以引入了泛型这个看门狗。---- 详细的我们到泛型哪里详细介绍。
只能说各种语言的设计、源码并不尽相同,越学到后面发现大佬们也是在做各种缝缝补补的工作,毕竟动源码市面上已经部署的哪些项目不就全部没了,后面也没有维护了(只能说成本太大)
而且也不尽是一件坏事,虽然Java泛型的这个想法只是原来为了缝补原来设计的一个大缺陷,但是后来发现又可以用在了其他地方。使得Java本来的一门强类型语言变得更加灵活。---- 泛型类、泛型方法这些,使得后来的语言强类型语言都保留了泛型这个设计,并没有从源码上改变。
public class Test {
public static void main(String[] args){
ArrayList list = new ArrayList();
list.add(1);
list.add("hello");
list.add(new Student(12,"张三"));
list.add(new Student(11,"李三"));
list.add(new Student(13,"王五"));
System.out.println(list);
// [1, hello, cn.hjblogs.demo.Student@682a0b20, cn.hjblogs.demo.Student@3d075dc0, cn.hjblogs.demo.Student@214c265e]
}
}
class Student{
String name;
int age;
int[] arr = new int[3];
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
}
【注】:这个就和Python列表很像了
2 ArryList常见成员方法
【注】:其中E表示ArryList里面元素数据类型 , e表示具体数据元素
(1)boolean add(E e) : 添加元素,返回值表示是否添加成功
(2)void add(int index, E e) :在指定索引位置插入元素。
(3)boolean remove(E e) : 删除第一个指定元素 e,返回值表示是否删除成功
(4)E remove(int index) : 删除指定索引元素,返回被删除元素
(5)E set(int index, E e) : 修改指定索引下的元素,返回原来的元素
(6)E get(int index) : 获取指定索引处的元素
(7)int size() : 返回集合的长度
// ArrayList<String> ls = new ArrayList<String>();
ArrayList<String> ls = new ArrayList<>();
ls.add("aaa");
ls.add("bbb");
ls.add("ccc");
ls.add("aaa");
System.out.println(ls); // [aaa, bbb, ccc, aaa]
ls.remove("aaa"); // 删除第一个aaa
System.out.println(ls); // [bbb, ccc, aaa]
ls.remove(1); // 删除索引为1的元素
System.out.println(ls); // [bbb, aaa]
ls.set(1, "ddd"); // 修改索引为1的元素
System.out.println(ls); // [bbb, ddd]
System.out.println(ls.get(1)); // ddd
System.out.println(ls.size()); // 2
(8)boolean isEmpty() :判断数组列表是否为空。
这个判断为空有专门方法,没有Python那么方便。
【注】:java中的 逻辑运算符 ! 和Python中的not有同样效果
ArrayList<String> list = new ArrayList<>();
list.add("Apple"); // 添加一个元素,使列表非空
// 判断是否非空
if (!list.isEmpty()) {
System.out.println("List is not empty");
} else {
System.out.println("List is empty");
}
3 ArryList的遍历
(1)使用get()方法遍历
(2)使用增强for循环遍历
ArrayList<String> list = new ArrayList<>();
list.add("Apple"); // 添加一个元素,使列表非空
list.add("Banana");
list.add("Orange");
// 使用get方法遍历列表
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
// 使用增强for循环遍历列表
for (String str : list) {
System.out.print(str + " ");
}
五、泛型
在上面Arrays、ArrayList的学习上我们已经见到泛型类、泛型方法,可以看到很灵活。Java语法规定数据类型每种都是固定的,就算有多态这种能稍微缓解一下子父类键数据类型不那么严格外,还是远远不如python那么灵活。如果用惯了python,就会发现如果强烈定死类型,的确有时候不太方便,所以Java就有了泛型这个概念,泛型顾明思议就是任意类型呗。
泛型的主要作用主要体现在两个方面:
- (1)看门狗:为了解决元素进集合的问题(为了解决JDK5前使用集合不使用泛型的一个巨大缺陷)
- (2)不指定特定类型,调用的时候才指定特定的类型(泛型类、泛型方法都可以用)
先说看门狗这个功能吧!第二个功能在讲泛型类、泛型方法的时候自然就知道了
1 JDK5前集合不使用泛型的缺陷
下面这个例子是不使用泛型,不同数据类型进集合的例子(现在任然可以用):
我们也可以不指定泛型,JDK5前没有泛型就是这么用的。不指定泛型,所以进集合ArryList的元素类型都是总父类Object,无论你是字符串对象还是包装类的什么,声明的变量类型都是Object。但很遗憾,虽然这样看起来和python列表很类似,但在Java里面有巨大风险,所以在JDK5以后由于Java也不想动源码,所以引入了泛型这个看门狗。
其实和:ArrayList<Object> list = new ArrayList<>(); 效果完全一样。
public class Test {
public static void main(String[] args){
ArrayList list = new ArrayList();
list.add(1);
list.add("hello");
list.add(new Student(12,"张三"));
list.add(new Student(11,"李三"));
list.add(new Student(13,"王五"));
System.out.println(list);
// [1, hello, cn.hjblogs.demo.Student@682a0b20, cn.hjblogs.demo.Student@3d075dc0, cn.hjblogs.demo.Student@214c265e]
}
}
class Student{
String name;
int age;
int[] arr = new int[3];
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
}
如果你用过python列表,你会觉得非常棒,这个效果太好了,所以还要泛型干什么?
很遗憾,虽然看起来和python列表一样,但在Java中存在一个巨大的缺陷:
- 问题:上面我的String如果没有用泛型限定进入集合的数据类型,那么进了集合后大家都会自动变成Object类型,拿出来的也是Object类型,这导致原本String里面独有的方法使用不了了。
此时想到的解决办法就是将父类强转成子类,使用强转转回来。由于强转需要知道你原来的数据类型,鬼知道你原来是什么数据类型,想强转也转不了。
大佬一想,你不就是想要知道进去的是什么数据类型吗?那我就给你用泛型定死不就得了,来个看门的,是指定类型就进。就算你进集合全部自动都要变成Object类型,没关系拿出来的时候我给你强转回来(泛型规定了类型,我就能知道你本来是什么类型)
这么一来问题就解决了,反正实际情况中,一个数据结构中也是存同一种数据类型的开发,压根用不到存多种数据类型。我定死加条狗就解决了。
public class Test {
public static void main(String[] args){
ArrayList<Student> list = new ArrayList<>(); // 指定泛型
list.add(new Student(12,"张三"));
list.add(new Student(11,"李三"));
list.add(new Student(13,"王五"));
System.out.println(list);
// [cn.hjblogs.demo.Student@404b9385, cn.hjblogs.demo.Student@6d311334, cn.hjblogs.demo.Student@682a0b20]
}
}
class Student{
String name;
int age;
int[] arr = new int[3];
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
}
【注】:Java中的泛型其实是一种假的泛型,添加进集合里面的元素只要一进集合就全部会变成Object类型,只是在拿出来的时候底层自动强转回了原来的数据类型
2 泛型类
泛型的设计不仅解决了集合里面的一个大缺陷,同样从这个名字可以看出 :可以指任意数据类型。什么意思?
- 我们编写类和方法的时候数据类型可以不用写死了,可以使用泛型在占位,创建对象的时候在由类或方法使用者确定应该是什么数据类型。这极大的增加了Java的灵活性。
下面就先来介绍泛型类
语法:
- 创建泛型类:public class Box<E>{}
public class Box<E,T>{}:多个泛型也是可以的
里面E可以是任意字符T A B C,反正大写就是了,尽量规范一点 - 使用泛型类:Box<String> stringBox = new Box<>();
创建泛型类
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
使用泛型类
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setValue("Hello");
System.out.println(stringBox.getValue());
}
}
泛型可以指定多个也可以,泛型类、泛型方法、泛型接口通用这个规则:
public class Cat<T,E>{
T name;
E age;
public Cat() {
}
public Cat(T name, E age) {
this.name = name;
this.age = age;
}
}
小练习
下面模拟一下ArrayList里面的 add和get方法用泛型类:
package cn.hjblogs.demo;
import java.util.Arrays;
public class MyArrayList<E> {
// 差一个自动扩容机制就和ArrayList没什么区别了
Object[] obj = new Object[10];
int size;
public boolean add(E e){
obj[size] = e;
size++;
return true;
}
public E get(int index){
return (E)obj[index]; // 根据泛型指定的类型强转回去
}
@Override
public String toString() {
return Arrays.toString(obj);
}
}
package cn.hjblogs.demo;
public class Test {
public static void main(String[] args){
MyArrayList<Student> list = new MyArrayList<>();
list.add(new Student(18, "张三"));
list.add(new Student(19, "李四"));
list.add(new Student(20, "王五"));
System.out.println(list);
// [cn.hjblogs.demo.Student@6d311334, cn.hjblogs.demo.Student@682a0b20, cn.hjblogs.demo.Student@3d075dc0, null, null, null, null, null, null, null]
System.out.println(list.get(1).name);
// 李四
}
}
class Student{
String name;
int age;
int[] arr = new int[3];
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
}
3 泛型方法
泛型方法根据使用类名后面的泛型和使用自己方法上的泛型有两种方案
- 语法一(使用类名后面的泛型)— 所有的方法都能用这个泛型:如果类名上定义了泛型 E,那么就可以使用类名上的泛型
修饰符 返回值类型 方法名(类型 变量名){}
例如:public E 方法名(参数){},这个E可以出现在返回值、参数、方法体中 - 语法二(在方法上声明自己的泛型)— 只有本方法可以使用该泛型:如果类名上面没有定义泛型(或者有),你可以在自己的方法上定义一个泛型
修饰符 <类型> 返回值类型 方法名(类型 变量名){}
例如:public <T> void show(T t){}, 这个T可以出现在返回值、参数、方法体中
【注】:泛型类一般用 E,方法独有的泛型一般用 T
小练习
- 定义一个工具类:ListUtil
类中定义一个静态方法addAll,用来一次性给集合ArrayLIst添加多个元素。
import java.util.ArrayList;
public class Add {
// 由于是工具类,建议私有化构造方法
private Add(){}
public static <T> void add(ArrayList<T> list, T e1, T e2, T e3){
// 要往集合里面添加元素,参数当然要传入一个集合 和 要添加的元素
// 本方法可以一次性添加三个元素
list.add(e1);
list.add(e2);
list.add(e3);
}
public static <T> void addAll(ArrayList<T> list, T... e1){
// 本方法可以一次性添加多个元素, 但是要注意可变参数的使用
for (T t : e1) {
list.add(t);
}
}
}
import java.util.ArrayList;
public class Test {
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
Add.addAll(list, "hello", "world", "java");
System.out.println(list); // [hello, world, java]
ArrayList<Integer> list2 = new ArrayList<>();
Add.addAll(list2, 1, 2, 3);
System.out.println(list2); // [1, 2, 3]
}
}
4 泛型接口
下面我们就分别看看方式1和方式2分别怎么使用一个带泛型的接口。
先看我们要实现的带泛型接口List,这是Java的一个内置接口。看源码:
可以看到这个接口是个<E>泛型接口(后面继承部分不用管),下面看实现类中用这两个接口的不同办法
- 方式一:实现类给出具体类型
public class MyList1 implements List<String> {
// 实现方法:略
}
public class Test {
public static void main(String[] args){
// 此时创建不用指定泛型,因为已经在类中规定过了String
MyList1 list = new MyList1();
}
}
- 方式二:实现类延续泛型,创建对象时再确定
public class MyList2<E> implements List<E> {
// 实现方法:略
}
public class Test {
public static void main(String[] args){
// 此时必须要泛型创建
MyList2<Integer> myList2 = new MyList2<>();
}
}
5 泛型不具备继承性(在方法传参中),但数据具备继承性
泛型不具备继承性(在方法传参中),但数据具备继承性
这句话是泛型的特点,前半句不好理解,我们先解释比较简单的后半句。
-
数据具备继承性:
在前面讲泛型的功能的时候讲过泛型的一个功能:看门狗。一个泛型类集合,假设泛型类型确定,那么进集合的可以是泛型中指定的具体类型和其对应的子类 -
泛型不具备继承性(在方法传参中)
这句话的关键在于括号内的内容,如果不看括号里面内容,压根就是迷糊的。下面解释
在前面学继承的时候再方法的传参里面有这么一个知识点应该记得:如果方法里面参数类型是父类,那么调用传参的时候可以传父类,也可以传其对应的子类也是可以的,这是一个典型的继承特性(多态性)
public class Test {
public static void main(String[] args){
method(new Son()); // method执行
method(new Father()); // method执行
}
public static void method(Father f){
System.out.println("method执行");
}
}
// 一个继承结构
class Father{}
class Son extends Father{}
但这个特性在泛型这里失效了
public class Test {
public static void main(String[] args){
ArrayList<Father> list1 = new ArrayList<>();
ArrayList<Son> list2 = new ArrayList<>();
method(list1); // method执行
method(list2); // 方法报错,因为method方法的参数是ArrayList<Father>类型,而list2是ArrayList<Son>类型;继承在泛型中不具备多态性
}
public static void method(ArrayList<Father> list){
System.out.println("method执行");
}
}
// 一个继承结构
class Father{}
class Son extends Father{}
6 有界类型参数(有应用场景 — 继承结构中)
前面泛型类,泛型方法,泛型接口都是泛型可以表示任意泛型。但实际中,任意数据类型实在太多了,有一个场景下,我们只想要一个继承体系下的父类及其所有子类类型。
- <T entends 父类>:调用时只能传递 父类或其子类:
使用 extends 关键字指定泛型类型参数的上界,可以是类或接口或者方法。
这里用泛型方法做一个示例(泛型类和泛型接口也是同样语法的):
public class Test {
public static void main(String[] args){
show(new Son()); // cn.hjblogs.demo.Son@6d311334
show(new Father()); // cn.hjblogs.demo.Father@682a0b20
String str = "hello";
show(str); // 报错,不在继承结构中传不进来
}
public static <T extends Father> void show(T e) {
// 限定了泛型的类型,只能是Father类或其子类
System.out.println(e);
}
}
// 一个继承结构
class Father{}
class Son extends Father{}
7 泛型通配符(看源码会碰到)
-----(了解,源码会看到,实际开发中不可能用到,Java的设计者我总感觉有一些强迫症,什么东西都做的特别细,衍生出了很多鸡肋的东西,关键源码里面他们还喜欢用这些鸡肋的东西)
这个使用场景就很有限了,没有6有界类型参数的使用方便,要用我也是用6.有界类型参数
(1)只能和集合一起使用
(2)只能在方法参数内使用
使用 ? 表示不确定的类型。主要有以下二种种情况(还有一种无界通配符?现在已经没有意义,后面版本估计就会抛弃了):
上界通配符:集合<? extends T> 表示 T 或 T 的子类型。
下界通配符:集合<? super T> 表示 T 或 T 的父类型。
public void processUpperBoundedList(List<? extends Father> list) {
// 表示调用时传入的参数只能是 List<Father>和List<Son>
for (Number elem : list) {
System.out.println(elem);
}
}
public void processLowerBoundedList(List<? super Son> list) {
// 表示调用时传入的参数只能是 List<Son>和List<Father>
for (Object elem : list) {
System.out.println(elem);
}
}
// 一个继承结构
class Father{}
class Son extends Father{}
【注】:List<? extends Father>这个功能使用6有界型参数完全也可以实现,非要搞出通配符这么个限制这么大的语法我是万万不太理解。