一、ArrayList
1.使用数组的局限性
比如:声明长度是10的数组,不用的数组就浪费了,超过10的个数,又放不下。
而ArrayList存放对象其容量会随着对象的增加,自动增长。
2.常用方法
(1)add 增加
//第一种是直接add对象,把对象加在最后面
heros.add(new Hero("hero " + i));
//第二种是在指定位置加对象
heros.add(3, specialHero);
(2)contains 判断一个对象是否在容器中
(3)get 获取指定位置的对象,如果输入的下标越界,会报错
(4)indexOf 用于判断一个对象在ArrayList中所处的位置
(5)remove 可以根据下标删除ArrayList的元素;也可以根据对象删除
//根据下标删除ArrayList的元素
heros.remove(2);
//根据对象删除
heros.remove(specialHero);
(6)set 用于替换指定位置的元素
heros.set(5, new Hero("hero 5"));
(7)size 用于获取ArrayList的大小
(8)toArray 可以把一个ArrayList对象转换为数组。
需要注意的是,如果要转换为一个Hero数组,那么需要传递一个Hero数组类型的对象给toArray(),这样toArray方法才知道,你希望转换为哪种类型的数组,否则只能转换为Object数组。
Hero hs[] = (Hero[])heros.toArray(new Hero[]{});
(9)addAll 把另一个容器所有对象都加进来
(10)clear 清空一个ArrayList
3.List接口
ArrayList实现了接口List,常见的写法会把引用声明为接口List类型。
因为ArrayList实现了List接口,所以List接口的方法ArrayList都实现了,所以不重复说明了。
4.泛型 Generic
不指定泛型的容器,可以存放任何类型的元素;指定了泛型的容器,只能存放指定类型的元素以及其子类。
//为了不使编译器出现警告,需要前后都使用泛型,像这样:
List<Hero> genericheros = new ArrayList<Hero>();
//不过JDK7提供了一个可以略微减少代码量的泛型简写方式
List<Hero> genericheros2 = new ArrayList<>();
5.遍历
(1)for循环
(2)增强for循环
(3)使用迭代器
不断用hasNext()判断是否还有下一个数据。
public class TestCollection {
public static void main(String[] args) {
List<Hero> heros = new ArrayList<Hero>();
//放5个Hero进入容器
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero name " +i));
}
//第二种遍历,使用迭代器
System.out.println("--------使用while的iterator-------");
Iterator<Hero> it= heros.iterator();
//从最开始的位置判断"下一个"位置是否有数据
//如果有就通过next取出来,并且把指针向下移动
//直到"下一个"位置没有数据
while(it.hasNext()){
Hero h = it.next();
System.out.println(h);
}
//迭代器的for写法
System.out.println("--------使用for的iterator-------");
for (Iterator<Hero> iterator = heros.iterator(); iterator.hasNext();) {
Hero hero = (Hero) iterator.next();
System.out.println(hero);
}
}
}
二、其他集合
序列分先进先出FIFO,先进后出FILO
FIFO在Java中又叫Queue 队列
FILO在Java中又叫Stack 栈
1.LinkedList 与 List接口
与ArrayList一样,LinkedList也实现了List接口,诸如add,remove,contains等等方法。
(1)双向链表 - Deque
除了实现了List接口外,LinkedList还实现了双向链表结构Deque,可以很方便的在头尾插入删除数据。
addFirst(); //在最前面插入新的元素
addLast(); //在最后面插入新的元素
getFirst(); //查看最前面的元素
getLast(); //查看最后面的元素
removeFirst(); //取出最前面的元素
removeLast(); //取出最后面的元素
(2)队列 - Queue
LinkedList 除了实现了List和Deque外,还实现了Queue接口(队列)。
Queue是先进先出队列 FIFO,常用方法:
offer 在最后添加元素
poll 取出第一个元素
peek 查看第一个元素
2.二叉树
二叉树由各种节点组成,其特点是:每个节点都可以有左子节点、右子节点,每一个节点都有一个值。
(1)二叉树排序-插入数据
插入基本逻辑是,小、相同的放左边,大的放右边
(2)二叉树排序-遍历
通过上一个步骤的插入行为,实际上,数据就已经排好序了。 接下来要做的是看,把这些已经排好序的数据,遍历成我们常用的List或者数组的形式
二叉树的遍历分左序,中序,右序
左序即: 中间的数遍历后放在左边
中序即: 中间的数遍历后放在中间
右序即: 中间的数遍历后放在右边
public class Node {
// 左子节点
public Node leftNode;
// 右子节点
public Node rightNode;
// 值
public Object value;
// 插入 数据
public void add(Object v) {
// 如果当前节点没有值,就把数据放在当前节点上
if (null == value)
value = v;
// 如果当前节点有值,就进行判断,新增的值与当前值的大小关系
else {
// 新增的值,比当前值小或者相同
if ((Integer) v -((Integer)value) <= 0) {
if (null == leftNode)
leftNode = new Node();
leftNode.add(v);
}
// 新增的值,比当前值大
else {
if (null == rightNode)
rightNode = new Node();
rightNode.add(v);
}
}
}
// 中序遍历所有的节点
public List<Object> values() {
List<Object> values = new ArrayList<>();
// 左节点的遍历结果
if (null != leftNode)
values.addAll(leftNode.values());
// 当前节点
values.add(value);
// 右节点的遍历结果
if (null != rightNode)
values.addAll(rightNode.values());
return values;
}
public static void main(String[] args) {
int randoms[] = new int[] { 67, 7, 30, 73, 10, 0, 78, 81, 10, 74 };
Node roots = new Node();
for (int number : randoms) {
roots.add(number);
}
System.out.println(roots.values());
}
}
3.HashMap
HashMap储存数据的方式是—— 键值对。
public class TestCollection {
public static void main(String[] args) {
HashMap<String,String> dictionary = new HashMap<>();
dictionary.put("adc", "物理英雄");
dictionary.put("apc", "魔法英雄");
dictionary.put("t", "坦克");
System.out.println(dictionary.get("t"));
}
}
对于HashMap而言,key是唯一的,不可以重复的。
所以,以相同的key 把不同的value插入到 Map中会导致旧元素被覆盖,只留下最后插入的元素。不过,同一个对象可以作为值插入到map中,只要对应的key不一样。
4.HashSet
Set中的元素,不能重复,没有顺序。
Set不提供get()来获取指定位置的元素,所以遍历需要用到迭代器,或者增强型for循环。
public class TestCollection {
public static void main(String[] args) {
HashSet<String> names = new HashSet<String>();
names.add("gareen");
System.out.println(names);
//第二次插入同样的数据,是插不进去的,容器中只会保留一个
names.add("gareen");
System.out.println(names);
}
}
通过观察HashSet的源代码,可以发现HashSet自身并没有独立的实现,而是在里面封装了一个Map。
HashSet是作为Map的key而存在的,而value是一个命名为PRESENT的static的Object对象,因为是一个类属性,所以只会有一个。
5.Collection
Collection是 Set List Queue和 Deque的接口
Queue: 先进先出队列
Deque: 双向链表
注:Collection和Map之间没有关系,Collection是放一个一个对象的,Map 是放键值对的
注:Deque 继承 Queue,间接的继承了 Collection
6.Collections
Collections是一个类,容器的工具类,就如同Arrays是数组的工具类。
(1)reverse 使List中的数据发生翻转
(2)shuffle 混淆List中数据的顺序
(3)sort 对List中的数据进行排序
(4)swap 交换两个数据的位置
(5)rotate 把List中的数据,向右滚动指定单位的长度
(6)synchronizedList 把非线程安全的List转换为线程安全的List
三、几种集合的关系与 区别
1.ArrayList和LinkedList的区别
ArrayList 插入,删除数据慢;
LinkedList 插入,删除数据快;
ArrayList 是顺序结构,所以定位很快,指哪找哪;
LinkedList 是链表结构,就像手里的一串佛珠,要找出第99个佛珠,必须得一个一个的数过去,所以定位慢。
2.HashMap和Hashtable的区别
HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式
区别1:
HashMap 可以存放 null
Hashtable 不能存放null
区别2:
HashMap 不是线程安全的类
Hashtable 是线程安全的类
3.HashSet、LinkedHashSet、TreeSet
HashSet: 无序
LinkedHashSet: 按照插入顺序
TreeSet: 从小到大排序
四、比较器与聚合操作
1.Comparator
简单说,就是让集合中的对象能按照你的意愿进行比较。
假设Hero有三个属性 name,hp,damage
一个集合中放存放10个Hero,通过Collections.sort对这10个进行排序
那么到底是hp小的放前面?还是damage小的放前面?Collections.sort也无法确定,所以要指定到底按照哪种属性进行排序。这里就需要提供一个Comparator给定如何进行两个对象之间的大小比较。
Comparator<Hero> c = new Comparator<Hero>() {
@Override
public int compare(Hero h1, Hero h2) {
//按照hp进行排序
if(h1.hp>=h2.hp)
return 1; //正数表示h1比h2要大
else
return -1;
}
};
2.Comparable
在类里面提供比较算法,无需额外提供比较器Comparator。
public class Hero implements Comparable<Hero>{
public String name;
public float hp;
public int damage;
public Hero(){
}
public Hero(String name) {
this.name =name;
}
//初始化name,hp,damage的构造方法
public Hero(String name,float hp, int damage) {
this.name =name;
this.hp = hp;
this.damage = damage;
}
@Override
public int compareTo(Hero anotherHero) {
if(damage<anotherHero.damage)
return 1;
else
return -1;
}
@Override
public String toString() {
return "Hero [name=" + name + ", hp=" + hp + ", damage=" + damage + "]\r\n";
}
}
3.聚合操作
JDK8之后,引入了对集合的聚合操作,可以非常容易的遍历,筛选,比较集合中的元素。
public class TestAggregate {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 10; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化集合后的数据 (最后一个数据重复):");
System.out.println(heros);
//传统方式
Collections.sort(heros,new Comparator<Hero>() {
@Override
public int compare(Hero o1, Hero o2) {
return (int) (o2.hp-o1.hp);
}
});
Hero hero = heros.get(2);
System.out.println("通过传统方式找出来的hp第三高的英雄名称是:" + hero.name);
//聚合方式
String name =heros
.stream()
.sorted((h1,h2)->h1.hp>h2.hp?-1:1)
.skip(2)
.map(h->h.getName())
.findFirst()
.get();
System.out.println("通过聚合操作找出来的hp第三高的英雄名称是:" + name);
}
}
五、泛型
1.集合中的泛型
2.类中的泛型
支持泛型的Stack。在类的声明上,加上一个<T>,表示该类支持泛型。
public class MyStack<T> {
LinkedList<T> values = new LinkedList<T>();
public void push(T t) {
values.addLast(t);
}
public T pull() {
return values.removeLast();
}
public T peek() {
return values.getLast();
}
public static void main(String[] args) {
//在声明这个Stack的时候,使用泛型<Hero>就表示该Stack只能放Hero
MyStack<Hero> heroStack = new MyStack<>();
heroStack.push(new Hero());
//不能放Item
heroStack.push(new Item());
//在声明这个Stack的时候,使用泛型<Item>就表示该Stack只能放Item
MyStack<Item> itemStack = new MyStack<>();
itemStack.push(new Item());
//不能放Hero
itemStack.push(new Hero());
}
}
3.通配符
(1)? extends
ArrayList heroList<? extends Hero> 表示这是一个Hero泛型或者其子类泛型。
heroList 的泛型可能是Hero
heroList 的泛型可能是APHero
heroList 的泛型可能是ADHero
所以 可以确凿的是,从heroList取出来的对象,一定是可以转型成Hero的。
但是,不能往里面放东西。
(2)? super
ArrayList heroList<? super Hero> 表示这是一个Hero泛型或者其父类泛型
heroList的泛型可能是Hero
heroList的泛型可能是Object
可以往里面插入Hero以及Hero的子类,但是取出来有风险,因为不确定取出来是Hero还是Object。
(3)泛型通配符?
泛型通配符? 代表任意泛型。既然?代表任意泛型,也就是说,这个容器什么泛型都有可能,所以只能以Object的形式取出来。
因此不能往里面放对象,因为不知道到底是一个什么泛型的容器
(4)总结
如果希望只取出,不插入,就使用? extends Hero
如果希望只插入,不取出,就使用? super Hero
如果希望,又能插入,又能取出,就不要用通配符?
六、lambda表达式
1.Hello lambda
假设一个情景: 找出满足条件(hp>100 && damage<50)的Hero
(1)普通方法
for (Hero hero : heros) {
if(hero.hp>100 && hero.damage<50)
System.out.print(hero);
}
(2)匿名类方法
interface HeroChecker {
public boolean test(Hero h);
}
public class TestLambda {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);
System.out.println("使用匿名类的方式,筛选出 hp>100 && damange<50的英雄");
HeroChecker checker = new HeroChecker() {
@Override
public boolean test(Hero h) {
return (h.hp>100 && h.damage<50);
}
};
filter(heros,checker);
}
private static void filter(List<Hero> heros,HeroChecker checker) {
for (Hero hero : heros) {
if(checker.test(hero))
System.out.print(hero);
}
}
}
(3)Lambda方法
public class TestLamdba {
public static void main(String[] args) {
Random r = new Random();
List<Hero> heros = new ArrayList<Hero>();
for (int i = 0; i < 5; i++) {
heros.add(new Hero("hero " + i, r.nextInt(1000), r.nextInt(100)));
}
System.out.println("初始化后的集合:");
System.out.println(heros);
System.out.println("使用Lamdba的方式,筛选出 hp>100 && damange<50的英雄");
filter(heros,h->h.hp>100 && h.damage<50);
}
private static void filter(List<Hero> heros,HeroChecker checker) {
for (Hero hero : heros) {
if(checker.test(hero))
System.out.print(hero);
}
}
}
(4)从匿名类到Lambda表达式
①匿名类的正常写法
HeroChecker c1 = new HeroChecker() {
public boolean test(Hero h) {
return (h.hp>100 && h.damage<50);
}
};
②把外面的壳子去掉,只保留方法参数和方法体,参数和方法体之间加上符号 ->
HeroChecker c2 = (Hero h) ->{
return h.hp>100 && h.damage<50;
};
③把return和{}去掉
HeroChecker c3 = (Hero h) ->h.hp>100 && h.damage<50;
④把参数类型和圆括号去掉(只有一个参数的时候,才可以去掉圆括号)
HeroChecker c4 = h ->h.hp>100 && h.damage<50;
⑤ 把c4作为参数传递进去
filter(heros,c4);
⑥直接把表达式传递进去
filter(heros, h -> h.hp > 100 && h.damage < 50);
与匿名类概念相比较,Lambda 其实就是匿名方法,这是一种把方法作为参数进行传递的编程思想。虽然Lambda表达式这样写,但是Java会在背后,悄悄的,把这些都还原成匿名类方式。
(5)Lambda表达式的弊端
Lambda表达式虽然带来了代码的简洁,但是也有其局限性。
①可读性差,与啰嗦的但是清晰的匿名类代码结构比较起来,Lambda表达式一旦变得比较长,就难以理解
② 不便于调试,很难在Lambda表达式中增加调试信息,比如日志
③版本支持,Lambda表达式在JDK8版本中才开始支持,如果系统使用的是以前的版本,考虑系统的稳定性等原因,而不愿意升级,那么就无法使用。
Lambda比较适合用在简短的业务代码中,并不适合用在复杂的系统中,会加大维护成本。
2.方法引用
(1)引用静态方法
首先为TestLambda添加一个静态方法:
public static boolean testHero(Hero h) {
return h.hp>100 && h.damage<50;
}
Lambda表达式:
filter(heros, h->h.hp>100 && h.damage<50);
在Lambda表达式中调用这个静态方法:
filter(heros, h -> TestLambda.testHero(h) );
调用静态方法还可以改写为:
filter(heros, TestLambda::testHero);
(2)引用对象方法
与引用静态方法很类似,只是传递方法的时候,需要一个对象的存在。
TestLambda testLambda = new TestLambda();
filter(heros, testLambda::testHero);
(3)引用容器中的对象的方法
首先为Hero添加一个方法
public boolean matched(){
return this.hp>100 && this.damage<50;
}
使用Lambda表达式
filter(heros,h-> h.hp>100 && h.damage<50 );
在Lambda表达式中调用容器中的对象Hero的方法matched
filter(heros,h-> h.matched() );
matched恰好就是容器中的对象Hero的方法,那就可以进一步改写为
filter(heros, Hero::matched);
(4)引用构造器
有的接口中的方法会返回一个对象,比如java.util.function.Supplier提供了一个get方法,返回一个对象。
public interface Supplier<T> {
T get();
}
设计一个方法,参数是这个接口
public static List getList(Supplier<List> s){
return s.get();
}
为了调用这个方法,有3种方式
第一种匿名类:
Supplier<List> s = new Supplier<List>() {
public List get() {
return new ArrayList();
}
};
List list1 = getList(s);
第二种:Lambda表达式
List list2 = getList(()->new ArrayList());
第三种:引用构造器
List list3 = getList(ArrayList::new);
3.聚合操作
(1)传统方式与聚合操作方式遍历数据
遍历数据的传统方式就是使用for循环,然后条件判断,最后打印出满足条件的数据
for (Hero h : heros) {
if (h.hp > 100 && h.damage < 50)
System.out.println(h.name);
}
使用聚合操作方式,画风就发生了变化:
heros
.stream()
.filter(h -> h.hp > 100 && h.damage < 50)
.forEach(h -> System.out.println(h.name));
(2)Stream和管道的概念
要了解聚合操作,首先要建立Stream和管道的概念
Stream 和Collection结构化的数据不一样,Stream是一系列的元素,就像是生产线上的罐头一样,一串串的出来。
管道指的是一系列的聚合操作。管道又分3个部分:
①管道源:在这个例子里,源是一个List
②中间操作: 每个中间操作,又会返回一个Stream,比如.filter()又返回一个Stream, 中间操作是“懒”操作,并不会真正进行遍历。
③结束操作:当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。 结束操作不会返回Stream,但是会返回int、float、String、 Collection或者像forEach,什么都不返回, 结束操作才进行真正的遍历行为,在遍历的时候,才会去进行中间操作的相关判断
注: 这个Stream和I/O章节的InputStream,OutputStream是不一样的概念。
(3)管道源
把Collection切换成管道源很简单,调用stream()就行了。
heros.stream()
但是数组却没有stream()方法,需要使用
Arrays.stream(hs)
//或者
Stream.of(hs)
(4)中间操作
每个中间操作,又会返回一个Stream,比如.filter()又返回一个Stream, 中间操作是“懒”操作,并不会真正进行遍历。
中间操作比较多,主要分两类:对元素进行筛选 和 转换为其他形式的流。
①对元素进行筛选:
filter 匹配
distinct 去除重复(根据equals判断)
sorted 自然排序
sorted(Comparator<T>) 指定排序
limit 保留
skip 忽略
②转换为其他形式的流:
mapToDouble 转换为double的流
map 转换为任意类型的流
(5)结束操作
当进行结束操作后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。 结束操作不会返回Stream,但是会返回int、float、String、 Collection或者像forEach,什么都不返回,。
结束操作才真正进行遍历行为,前面的中间操作也在这个时候,才真正的执行。
常见结束操作如下:
forEach() 遍历每个元素
toArray() 转换为数组
min(Comparator<T>) 取最小的元素
max(Comparator<T>) 取最大的元素
count() 总数
findFirst() 第一个元素