- 导语
- List、Set、Map
- lambda表达式、流式操作
- 泛型
一、导语
在第一节课中,我们讲了数组这一存储多个元素的数据结构,可以方便我们在编程中用一个对象来储存多个元素。然而在实际应用时,数组有一些缺点 ——
- 数组的长度一旦确定就无法更改。
- 增加和删除的效率低下。
- 根据内容查找元素速率慢。
事实上,对于很多实际开发中的数据结构,单纯用数组表示是比较麻烦且不易维护的,于是 Java 为我们提供了集合类来方便我们的操作。Java 集合类包含在 java.util 包下,所以在使用集合类前,要添加导包语句 import java.util.*;
二、List、Set、Map
-
List
一个 List 是一个元素有序的、可以重复、可以为 null 的集合(有时候我们也叫它“序列”)。Java 集合框架中最常使用的几种 List 的实现类是 ArrayList,LinkedList 和 Vector。ArrayList 的特点是可以动态添加元素,并像可以像数组一样通过下标访问对应的元素。我们接下来会介绍 ArrayList 的使用。
-
ArrayList
ArrayList是基于数组实现的List类,它封装了一个动态的增长的、允许再分配的Object[]数组。public class ArrayListTest { public static void main(String[] args) { List<String> arrayList = new ArrayList<>(); //创建一个 ArrayList 对象 arrayList.add("1"); arrayList.add("12"); arrayList.add("123"); //向 arrayList 的最后添加元素 arrayList.add(0, "0"); //在 arrayList 的指定下标处插入元素 System.out.println(arrayList); String str = arrayList.get(1); //str = "1" //访问 arrayList 指定下标index对应的元素 int length = arrayList.size(); //length = 4 //返回 arrayList 中元素的个数 boolean isContain = arrayList.contains("123"); //isContain = true //判断 arrayList 中是否存在某个元素值 arrayList.remove(0);//将arrayList中下标为0的元素移除 System.out.println(arrayList); arrayList.remove("123");//将arrayList中"123"元素移除 System.out.println(arrayList); arrayList.clear();//将arrayList中所有元素移除 boolean isEmpty = arrayList.isEmpty();//isEmpty = true //判断 arrayList 是否为空 } }
( 注:1. 在创建 ArrayList对象时,尖括号中<>若不填任何类型或接口,则 ArrayList 可以容纳任何类型的元素,若规定了类型,则 ArrayList 只能容纳该类型的对象。 2. 在调用 ArrayList 的 get 和 remove 方法时,如果传进去的参数 index ≥ arrayList.size() 或 index < 0 程序会停止运行,并抛出异常(Exception)。3. 在调用 ArrayList 的 remove 方法时(删除 arrayList 中的某个元素), 如果原来的 ArrayList 中包含这个元素, ArrayList 会将这个元素删除并返回 true,若不包含,则直接返回false。)
-
Set
Set 是对数学上集合概念的抽象。一个 Set 是一个元素无序的、不可以重复,可以为 null 的集合。与 List 不同的是,我们不能通过下标来对 Set 进行操作,且一个 Set 最多可含一个 null 元素,对于任意的非 null 元素 e1 和 e2,都满足
e1.equals(e2) == false
。Set 有三个实现类 —— HashSet、LinkedHashSet、TreeSet。接下来以 HashSet 为例来介绍 Set 的用法。
-
HashSet
HashSet是Set接口的典型实现,HashSet使用HASH算法来存储集合中的元素,因此具有良好的存取和查找性能。当向HashSet集合中存入一个元素时,HashSet会调用该对象的
值得主要的是,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法的返回值相等
ArrayList 中所有参数未涉及下标(index)的函数都可以类推到 Set
两个 Set 取并集
set1.addAll(set2)/取并集的结果为 set1
两个 Set 取交集
set1.retainAll(set2)//取交集的结果为 set1
-
Set 的遍历
public class HashSetTest { public static void main(String[] args) { HashSet set = new HashSet<String>(); //初始化一个Set set.add("12"); set.add("1"); set.add("12"); set.add("123"); set.add("123");//重复的元素不会被添加 set.add("13"); set.add("23"); set.add("42"); Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } //输出结果 //12 //1 //23 //123 //13 //42 //注:set是元素无序的 } }
-
Map
Map 是对数学上映射概念的抽象。Map 中的每个元素都包含两部分 —— 键(key)和值(value),可以通过键来获取值。在一个 Map 中,key 的值是不可以重复的。Map 有两个实现类 —— HashMap、TreeMap。接下来以 HashMap 为例来介绍 Map 的用法。
-
HashMap
就像HashSet集合不能保证元素的顺序一样,HashMap也不能保证key-value对的顺序。并且类似于HashSet判断两个key是否相等的标准也是: 两个key通过equals()方法比较返回true、 同时两个key的hashCode值也必须相等-
clear 、size、isEmpty 函数和 ArrayList 类似。
public class HashMapTest { public static void main(String[] args) { HashMap hashMap = new HashMap<Integer, String>();//初始化一个key类型为int,value类型为String的Map hashMap.put(1, "My key is 1."); hashMap.put(2, "My key is 2."); hashMap.put(3, "My key is 3."); //为map添加一个key为3,value为"My key is 3."的元素 System.out.println(hashMap.keySet()); // 输出结果:[1, 2, 3] // 返回了hashMap中所有key的集合(Set) System.out.println(hashMap.values()); // 输出结果:[My key is 1., My key is 2., My key is 3.] // 返回了hashMap中所有value的集合(Collection) System.out.println(hashMap.entrySet()); //输出结果:[1=My key is 1., 2=My key is 2., 3=My key is 3.] // 返回了hashMap中所有映射关系的集合(Set) HashMap<Integer, String> hashMap1 = new HashMap<>(); hashMap1.put(4, "My key is 4"); hashMap1.put(5, "My key is 5"); hashMap.putAll(hashMap1);//将 hashMap1 中的所有元素加到hashMap中 System.out.println(hashMap.remove(1)); // 输出结果:My key is 1. // 删除 hashMap 中key为1的元素,并返回key为1的value值 } }
-
-
队列
队列的头部保存着队列中存放时间最长的元素,队列的尾部保存着队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素,队列不允许随机访问队列中的元素。结合生活中常见的排队就会很好理解这个概念三、lambda表达式、流式操作符
Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体
基本语法:
(parameters) -> expression
或
(parameters) ->{ statements;}
简例:
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
看看用法:
interface Operation {
int operate(int a, int b);
}
public static int operate(int a, int b, Operation o){
o.operate(a, b);
}
public static void main(String []args){
Operation o = (int a, int b) -> a - b;
System.out.println(operate(3, 2, o)); //输出 1
}
用lambda表达式实现匿名内部类(Runnable 接口)
// 1.1使用匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello world !");
}
}).start();
// 1.2使用 lambda expression
new Thread(() -> System.out.println("Hello world !")).start();
集合的流式操作:
Stream :
Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda表达式,极大的提高编程效率和程序可读性。
示例:
List<StringClass> list1 = new ArrayList<>();
FirstData first = new FirstData();
for (int i = 0; i < first.getDataList().size(); i++){
SecondData secondData = first.getDataList().get(i);
//do something with second and firstData.
for (int j = 0; j < secondData.getDataList().size(); j++ ){
ThirdData third = secondData.getDataList().get(j);
//give thirdList a filter
if (third.getThirdData().startsWith(" ")){
list1.add(new StringClass(third.getThirdData()));
}
//do something with third and thirdData
}
}
first.getDataList()
.forEach(it ->
it.getDataList()
.stream()
.filter(t -> t.getThirdData().startsWith(" "))
.map(t -> new StringClass(t.getThirdData()))
.forEach(list1::add)
);
四、泛型
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。使用泛型,可以增加我们代码的复用性。
先看一个例子:
List arrayList = new ArrayList</*String*/>();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
System.out.println("item = "+item);
}
毫无疑问,程序的运行结果会以崩溃结束:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
List<String> arrayList = new ArrayList<String>();
...
//arrayList.add(100); 在编译阶段,编译器就会报错
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
-
泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
实例
public class Box<T> { public Box(T t){ this.t = t; } private T t; public void set(T t) { this.t = t; } public T get() { return t; } public static void main(String[] args) { Box<Integer> integerBox = new Box<>(); Box<String> stringBox = new Box<>(); integerBox.set(10); stringBox.set("天外天移动Android组"); System.out.println(integerBox.get()); System.out.println(stringBox.get()); //输出结果: //10 //天外天移动Android组 } }
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
public class Box<T> {
private T t;
public Box(T t){
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box integerBox = new Box<>(10);
Box stringBox = new Box<>("天外天移动Android组");
System.out.println(integerBox.get());
System.out.println(stringBox.get());
//输出结果:
//10
//天外天移动Android组
}
}
-
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
当实现泛型接口的类,传入泛型实参时:
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
- 类型通配符
在Box类的基础上,我们增加一个函数,将box之中的值打印出来:
//不是一个泛型方法
public void showKeyValue1(Box<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
此处的?就是类型通配符,可以让我们传入不同泛型对应的Box类。
-
泛型方法
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<T>)。
每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
泛型方法体的声明不能是原始类型(比如int 、double、char)。
实例
public class GenericsMethodTest {
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
private static<T> void printClassName(T t){
System.out.println(t.getClass());
}
public static void main(String[] args) {
printClassName(1);
printClassName(2.0);
printClassName("twt");
//输出结果:
//class java.lang.Integer
//class java.lang.Double
//class java.lang.String
}
}
- 泛型上下边界
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
- 为泛型通配符添加上边界,即传入的类型实参必须是指定类型的子类型
public void showKeyValue1(Box<? extends Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Box<String> box1 = new Box<String>("11111");
Box<Integer> box2 = new Box<Integer>(2222);
Box<Float> box3 = new Box<Float>(2.4f);
Box<Double> box4 = new Box<Double>(2.56);
//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(box1);
showKeyValue1(box2);
showKeyValue1(box3);
showKeyValue1(box4);
- 为方法的泛型添加上边界
//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Box<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}
代码来源
本文中的所有代码均来自自己写的一个 Java demo,已上传至 github 。