文章目录
- 前言
- 一、泛型
- 1.1 为何会有泛型的出现
- 1.2 泛型的使用
- 二、List的使用方法
- 2.1 常见方法举例
- 2.2 ArrayList 和 LinkedList 的区别
- 2.3 ArrayList 的add()方法背后的实现
- 三、经典例题
- 3.1 打印属性
- 3.2 删除字符
- 3.3 扑克牌练习
前言
在 Java 的类库中提供了很多的容器(container)
来帮助我们解决许多具体的问题。
本节就来总结一下 List
,主要介绍泛型
,List 的常见使用方法
和部分方法背后的实现
,以及相关练习
一、泛型
1.1 为何会有泛型的出现
泛型
是为了解决某些容器、算法等代码的通用性而引入,并且能在编译期间做类型检查。
为了解释泛型,举个例子,让我们写一个只能添加元素,获取元素的不太通用的顺序表
📑代码示例:
//自我实现的顺序表简单例子
class MyArrayList {
public int[] array;//顺序表底下的数组
public int usedSize;//顺序表中元素的个数
public MyArrayList() {
this.array = new int[15];//数组初始化
}
//增加元素
public void add(int value){
this.array[this.usedSize] = value;
this.usedSize++;
}
//通过索引获取元素的内容
public int get(int pos) {
return this.array[pos];
}
}
public class TestDemo {
public static void main(String[] args) {
MyArrayList myArrayList = new MyArrayList();
myArrayList.add(12);
myArrayList.add(21);
int ret = myArrayList.get(0);
System.out.println(ret);
}
}
事实上,这样的顺序表是非常不通用的
这只是一个可以存放整数的顺序表,如果使用 add 方法添加浮点型的元素必将会报错,那么想要得到一个可以存放浮点型的顺序表是不是意味着就要再写一个新的顺序表呢?
不然,所有基本数据类型的父类
都是 Object
,只要将 MyArrayList 中的 int 类型都改成 Object 就可以实现想往顺序表中放什么类型就放什么类型的想法
📑代码示例:
class MyArrayList {
public Object[] array;
public int usedSize;
public MyArrayList() {
this.array = new Object[15];
}
public void add(Object value){
this.array[this.usedSize] = value;
this.usedSize++;
}
public Object get(int pos) {
return this.array[pos];
}
}
public class TestDemo {
public static void main(String[] args) {
MyArrayList myArrayList = new MyArrayList();
myArrayList.add(12);//可以放入整形
myArrayList.add(21.3);//可以放入浮点型
int ret = (int)myArrayList.get(0);//一定要进行强制类型转换
System.out.println(ret);
}
}
事实上,这样的顺序表像个“垃圾桶”似的
各种各样的类都可以放在同一个顺序表中,并且就像代码里写的那样,想要获取该顺序表中下标为 0 的元素,一定要进行强制类型转化才可以获得该值,自相矛盾的是我们的目的就是为了知道下标为 0 的元素是什么,哪能知道那元素是啥类型,又如何强制类型转换呢?
泛型的出现就解决了以上的问题!!!
1.2 泛型的使用
📑代码示例:
public interface List<E> extends Collection<E>
接口 List 继承 Collection 接口,后面的尖括号 <>
是泛型的标志,E
是类型变量一般需要大写
, 泛型类可以一次有多个类型变量,用逗号
分割
如果将泛型运用到上述的顺序表中,应该是这样的
📑代码示例:
class MyArrayList<E> {
public E[] array;
public int usedSize;
public MyArrayList() {
this.array = (E[])new Object[15];
}
public void add(E value){
this.array[this.usedSize] = value;
this.usedSize++;
}
public E get(int pos) {
return this.array[pos];
}
}
public class TestDemo {
public static void main(String[] args) {
//该顺序表只能放 int 类型的数据
MyArrayList<Integer> myArrayList = new MyArrayList<>();
myArrayList.add(12);
int ret = myArrayList.get(0);
System.out.println(ret);
//该顺序表只能放 double 类型的数据
MyArrayList<Double> myArrayList1 = new MyArrayList<>();
myArrayList1.add(21.3);
}
}
🏸 代码结果:
通过泛型,向 <> 中传入不同的类型,就可以创建不同的顺序表,类型可以是基本类型对应的包装类
,也可以是自定义的类型
。传入类型后,放元素时是根据 <> 中指定的类型进行检查
,类型有误就会报错。另外,泛型编译运用的是一种擦除机制
,即在编译的时候,会将类型擦除成 Object
类型,等到运行的时候,就没有泛型的概念,个个都是 Object 类
如果不传入类型,就意味着一夜回到解放前,顺序表又成了“垃圾桶”,又要强制类型转换
二、List的使用方法
2.1 常见方法举例
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
//ArrayList<Integer> list = new ArrayList<>();
//想要实现一个 ArrayList 类,可以通过接口引用具体的一个实现类,也可以具体类具体实现
//例一
list.add(1);
list.add(2);
list.add(2);
System.out.println(list);//[1,2,2]
//例二
list.add(0,3);
System.out.println(list);//[3,1,2,2]
//例三
list.remove(1);
System.out.println(list);//[3,2,2]
//例四
System.out.println(list.get(1));//2
//例五
System.out.println(list.set(0, 5));//3
//例六
System.out.println(list.contains(5));//true
//例七
System.out.println(list.indexOf(2));//1
//例八
System.out.println(list.lastIndexOf(2));//2
//例九
List<Integer> list1 = list.subList(1,3);
System.out.println(list1);//[2,2]
//例十
list.addAll(list1);
System.out.println(list);//[5,2,2,2,2]
}
}
🏸 代码结果:
💬代码解释:
- 例一
boolean add(E e)
将指定的元素追加到此列表的末尾- 例二
void add(int index, E element)
在此列表的指定位置插入指定的元素- 例三
E remove(int index)
删除该列表中指定位置的元素- 例四
E get(int index)
返回此列表中指定位置的元素- 例五
E set(int index, E element)
用指定的元素替换此列表中指定位置的元素,返回值为 Index 位置原来的元素- 例六
boolean contains(Object o)
如果此列表包含指定的元素,返回 true- 例七
int indexOf(Object o)
返回列表中指定元素的第一次出现的索引,如果此列表不包含此元素,返回-1- 例八
int lastIndexOf(Object o)
返回列表中指定元素的最后一次出现的索引,如果此列表不包含此元素,返回-1- 例九
List<E> subList(int fromIndex, int toIndex)
截取列表中的一部分,索引返回为 [fromIndex,toIndex)
需要注意的是可以通过新生成的列表修改原列表,因为此处的新列表并不是拷贝得来的,而是直接指向原列表- 例十
boolean addAll(Collection<? extends E> c)
按指定集合的 Iterator(迭代器) 返回的顺序将指定集合中的所有元素追加到此列表的末尾
传过来的参数一定实现了 Collection 接口,是当前 List 指定的类型或者指定类型的子类
2.2 ArrayList 和 LinkedList 的区别
- 从底层的实现来说:
ArrayList 实际上是一个顺序表,底层是一个动态数组
LinkedList 实际上是一个双向链表
- 从方法的效率来说:
对于get() 和 set() 方法(随机访问)
来说,ArrayList 优于 LinkedList ,因为前者是一个数组,通过索引访问很便捷,后者只能按照顺序从列表的一端通过一个一个访问节点的数据域来检查
对于add() 和 remove() 方法(增删)
来说,LinkedList 优于 ArrayList,因为前者操作后会对数据的索引产生影响,如果一个元素被加到 ArrayList 的最开端时,所有已经存在的元素都会向后移动,在数据移动和复制上的开销会非常大,且和数组的大小成正比。后者就是需要改变节点的前驱和后继就可以实现增删,增加元素的开销是固定的- 从容量的自由度和空间浪费来说:
ArrayList
容量是需要进行设置的,因此自由度较低,且列表的结尾会预留一定的容量空间造成一定的空间浪费LinkedList
容量能够动态的随数据量的变化而变化,因此自由度较高,其空间花费在于每个元素都需要消耗一定的空间
2.3 ArrayList 的add()方法背后的实现
在上面 ArrayList 和 LinkedList 的比较中,有提到 ArrayList 的容量空间的问题,那么我们就通过 add() 方法来深入代码,看看 ArrayList 究竟是如何进行容量空间的增加的
注:想要深入代码,就需要按住 Ctrl 键
,同时点击想要深入的对象
上面的代码调用的都是 ArrayList 不带参数的构造方法,深入该方法。。。
构造方法的源码上的绿字儿说:构造一个初始容量为10的空列表
那么初识容量究竟是怎么产生的呢?
深入后发现 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
竟然是一个空的数组,按理来说,往空的数组中放数据会造成数组下标越界异常,此处没异常说明增加元素时一定进行了扩容,深入 ArrayList 的 add()方法。。。
总结:
- ArrayList 的
大小最初为0
,当第一次调用 add() 方法
时最终通过 grow 扩容函数,将容量扩大为10
- 当放满10个元素后,就会以
1.5倍
的速度扩容
三、经典例题
3.1 打印属性
题目:
书架上有很多的书(将书这个对象放在一个 List 当中),每本书共同拥有的属性是书名(String)、作者(String)、价格(double),现在想要将每本书的属性打印出来。
(本题实际上就是向泛型中传入自定义类型,然后打印其属性)
📑代码示例:
class Book {
public String name;
public String author;
public double price;
//带三个参数的构造方法
public Book(String name, String author, double price) {
this.name = name;
this.author = author;
this.price = price;
}
//重写 toString 方法
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}
public class TestDemo {
public static void main(String[] args) {
List<Book> list = new ArrayList<>();
list.add(new Book("西游记","吴承恩",23.5));
list.add(new Book("红楼梦","曹雪芹",19.8));
list.add(new Book("三国演义","罗贯中",54.6));
list.add(new Book("水浒传","施耐庵",34.5));
for (Book s:list) {
System.out.println(s);
}
}
}
//如果想要进行排序,就需要自己提供比较方法
3.2 删除字符
题目:
删除第一个字符串当中出现的第二个字符串中的字符
例如:String str1 = “hello world”; String str2 = “word”; 结果为:hell l
在没有学过 List 之前,对字符串进行操作我们的思路一般是这样的。。。
📑代码示例1:
public class TestDemo {
public static void func(String str1,String str2) {
//注意各种情况的发生
if (str1 == null || str2 == null) return;
StringBuffer str = new StringBuffer();
for (int i = 0; i < str1.length(); i++) {
//获取下标索引为 i 的字符
char ch = str1.charAt(i);
//加一个空字符串,非常巧妙的将字符 ch 变成字符串
//判断该字符串是否被str2包含,如若没包含就将其追加到str上
if (!str2.contains(ch + "")) {
str.append(ch);
}
}
//将StringBuffer类的str转化为String类
System.out.println(str.toString());
}
public static void main(String[] args) {
String str1 = "hello world";
String str2 = "word";
func(str1,str2);
}
}
这里运用的字符串相关知识,如果想要进行了解,指路——>字符串详解
学过 List 之后,解该题的思路是这样的(实际上总体和上面差不了太多)。。。
📑代码示例2:
public class TestDemo {
public static void fun2(String str1,String str2) {
if (str1 == null || str2 == null) return;
//创建一个列表,存放str1中没有被str2包含的字符
List<Character> list = new ArrayList<>();
for (int i = 0; i < str1.length(); i++) {
char ch = str1.charAt(i);
if(!str2.contains(ch + "")) {
list.add(ch);
}
}
//打印列表中的字符们
for (char ch:list) {
System.out.print(ch);
}
}
public static void main(String[] args) {
String str1 = "hello world";
String str2 = "word";
fun2(str1,str2);
}
}
3.3 扑克牌练习
实现内容:
- 可以买一副牌,不包括大小王,从1~13,四种花色,共52张牌
- 可以将买到的牌进行洗牌操作,打乱
- 可以进行发牌操作,这里实现三个人轮流揭牌,共揭5轮,共15张牌
📑代码示例:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
class Card {
//牌的花色
public String suit;
//牌的数字
public int number;
//提供带两个参数的构造方法
public Card(String suit, int number) {
this.suit = suit;
this.number = number;
}
//重写toString()方法
@Override
public String toString() {
return "{" + suit + "," + number +
"}";
}
}
public class PlayingCard {
//定义一个数组来存放四种花色
public static final String[] suits = {"♥","♦","♠","♣"};
//买牌
public static List<Card> buyCard(){
List<Card> cards = new ArrayList<>();
for (int i = 1; i <= 13; i++) {
for (int j = 0; j < 4; j++) {
cards.add(new Card(suits[j],i));
//按一定的顺序往cards列表中传入扑克牌
}
}
return cards;
}
//将cards列表中索引x处的牌和索引y处的牌进行交换
public static void swap(List<Card> cards,int x,int y) {
Card tmp = cards.get(x);
cards.set(x,cards.get(y));
cards.set(y,tmp);
}
//洗牌
//生成随机数,最开始,变量i为cards列表的最后一张扑克牌的索引
public static void shuffleCard(List<Card> cards) {
for (int i = cards.size() - 1; i > 0; i--) {
Random random = new Random();
//随机数生成范围为[0,i)
int num = random.nextInt(i);
//进行交换
swap(cards,i,num);
}
}
//发牌
//创建汇集三个人的扑克牌列表的总列表list
public static List<List<Card>> dealCard(List<Card> cards) {
List<Card> list1 = new ArrayList<>();
List<Card> list2 = new ArrayList<>();
List<Card> list3 = new ArrayList<>();
List<List<Card>> list = new ArrayList<>();
list.add(list1);
list.add(list2);
list.add(list3);
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 3; j++) {
//获取list列表中的第j个元素(三个人中的某一个人的扑克牌列表)
//从洗好的扑克牌中移除第1张(索引为0)添加到某一人的扑克牌列表中
list.get(j).add(cards.remove(0));
}
}
return list;
}
public static void main(String[] args) {
System.out.println("买牌");
List<Card> cards = buyCard();
System.out.println(cards);
System.out.println("洗牌");
shuffleCard(cards);
System.out.println(cards);
System.out.println("发牌");//规则为给三个人发牌,一共发五轮,共发15张牌
List<List<Card>> list = dealCard(cards);
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
🏸 代码结果:
完!