说明
这是一个完善了但又不完善的笔记,或许以后会更新
可以参考但请务必超越
源文件
Tools
数据结构:List
芜湖,爽爆的第一篇文章(数据结构当理解后,其实都不难)
为了更好更方便的学习,先来简单认识下泛型,毕竟已经接触到了。深入的话就之后再说吧
1.认识泛型
在这里简单先引入一个概念,泛型
简单认识一下,了解即可,看的源码就行
泛型的意义:
- 自动对类型进行检查
- 自动对类型进行强制类型转换
泛型类型不参与类型的组成。泛型尖括号当中的内容,不参与类型的组成
举栗
还是举栗学习,先写一个简单通用的顺序表
class MyArrayList {
private int[] elem;
private int usedSize;
public MyArrayList() {
this.elem = new int[10];
}
public void add(int value) {
this.elem[usedSize] = value;
usedSize++;
}
public int get(int pos) {
return this.elem[pos];
}
}
现在就有问题了,这只能存放整数的数据,不通用
能不能什么类型的数据都可以放?
当然可以,所有类的父类是Object
,那全改成Object
不就全都可以放了吗
class MyArrayList {
private Object[] elem;
private int usedSize;
public MyArrayList() {
this.elem = new Object[10];
}
public void add(Object value) {
this.elem[usedSize] = value;
usedSize++;
}
public Object get(int pos) {
return this.elem[pos];
}
}
可是这问题又来了,这不就和Collection
一样了吗
MyArrayList myArrayList = new MyArrayList();
myArrayList.add(10);
myArrayList.add("hello");
而且每次取数据的时候,都需要强制类型转换
由此可以引导出三个问题
- 能不能指定顺序表的类型。也就是调用者用的时候指定。
- 指定类型后,是不是只能放指定类型的数据?
- 取出数据能不能不要转换
使用泛型
前面有接触过,尖括号<>
使用一个<E>
代表当前类是一个泛型类,此时的这个E本质是一个占位符
class MyArrayList<E>
MyArrayList<String> myArrayList = new MyArrayList();
MyArrayList<Integer> myArrayList = new MyArrayList();
这样就可以把类型作为参数传递,也就是类型参数化
面试问题
泛型是怎么编译的?
泛型是编译(时)期的一种机制,擦除机制。(Object)
这其实很好理解,我们验证一下即可
前面把类型都改成了Object
,MyArrayList
添加了一个<E>
。那是不是说可以把Object
替换为E
类型
class MyArrayList<E> {
private E[] elem;
private int usedSize;
public MyArrayList() {
this.elem = (E[])new Object[10];
}
public void add(E value) {
this.elem[usedSize] = value;
usedSize++;
}
public E get(int pos) {
return this.elem[pos];
}
}
没有提示错误
那么我们看一下MyArrayList
字节码
除了最上面我们写的代码说明,里面的内容全部被擦为了Object
再看一下main
的字节码
new
出来的MyArrayList
没有<E>
实际运行打印也不会出现,说明直接擦除了
Object
数组和泛型
这里还有一个重要的点
为什么不可以new E[10]
而是要强转(E[])new Object[10]
呢
和上一篇集合文章中提到的一样
因为:JVM对数组和泛型的处理,数组和泛型之间的一个重要区别是他们如何强制执行类型检查。数组在运行时存储和检查类型信息,泛型在编译时检查
假设new E[10]
是成立的
public <T> T[] getArray(int size) {
T[] genericArray = new T[size];
return genericArray;
}
这个代码就会通过擦除机制变成这样
public Object[] getArray(int size) {
Object[] genericArray = new Object[size];
return genericArray;
}
有这个代码那必然会这样去写
MyArrayList<String> myArrayList = new MyArrayList();
String[] rets = (String[]) myArrayList.getArray(10);
写出来虽然没有问题但是必定会报错
包括(E[])new Object[10]
也是不对的,这样只是为了验证擦除机制
那到底要怎么写才可以呢,其实真正想要这样写,需要反射。注意,了解即可
这里还有一篇较好的文章,其中举栗抛错的抛错分析说的很好:关于Object[]
强制类型转换的思考
当然,Java不是还有ArrayList
吗,我们可以看看ArrayList
的做法
ArrayList
的泛型类型转换
点开ArrayList
可以看到他的数组也是Object
数组,那他是怎么做到没有异常的呢?
alt+7
或者搜索看一下其中的方法,比如说get
方法
是E类型,这怎么做到的呢?
return
了一个elemenData
,打开看看
原来如此,这是把单个的元素强转成E,这和我们直接整体强转是不一样的
这样就可以验证之前关于泛型类型的问题了
真滴酸爽,等到泛型篇章的时候,就可以彻底的深入了解了
2.包装类
在讲基本数据类型的时候提到过
针对8种基本数据类型,因为不是对象,那岂不是泛型机制就会失效?
事实上也确实如此,为了解决这个问题,Java引入了一种特殊的类,即8种基本数据类型的包装类。在使用过程中,会将基本数据类型的值包装到一个对象中去。
Java一切即对象,这样基本数据类型也就面相对象了
2.1 基本数据类型和其包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
只需要特殊记下Integer
和Character
即可,其他的都是首字母大写
2.2 包装类的好处
使用包装类就可以使用其Java提供的方法来进行一系列的操作,不用我们自己去实现,很方便
valueOf()
:
String str = "10";
int ret = Integer.valueOf(str);
System.out.println(ret + 10);
2.3 包装类的使用,装箱和拆箱
装箱(装包):把简单类型变成包装类类型
拆箱(拆包):把包装类类型变成简单类型
Integer a = 10;//装箱
int b = a;//拆箱
这样的方式属于隐式的装箱拆箱方式,因为在底层还是调用了valueOf()
来进行操作
那么直接使用valueOf()
也就是显式的方式了
Integer a1 = Integer.valueOf(20);
Integer a2 = new Integer(20);
int b1 = a1.intValue();
double b2 = a2.doubleValue();
自动装箱和自动拆箱
在使用包装类的时候,装箱和拆箱确实有不少的代码量,所以为了减少开发者负担,Java提供了自动机制
注意:自动装箱和自动拆箱是编译期间的一种机制
int i = 10;
Integer ii = i;//自动装箱
Integer ij = (Integer) i;//自动装箱
int j = ii;//自动拆箱
int k = (int) ii;//自动拆箱
这可太好了...
没事干的时候,就多去看看源码,多做做实验
常用的源码都可以看看,就可以知道这些东西了
当然还有一种方法可以查看,那就是前面一直都有在用的反编译工具javap
,用来反编译字节码
2.4 javap
反编译工具
关于完整的javap
,这里就不多说了,可以查,都可以查。以后也可能会讲。这里只用来看一下装箱和拆箱的过程
javap -c classname
太过细致的不用考虑,不过深究也是可以的
这里可以看到隐式和显示的装箱拆箱,自动拆箱和自动装箱
底层都是通过valueOf()
来进行操作,就可以说明问题了
2.5 缓存数组
面试问题(这是一道阿里的面试题,很重要)
Integer a = 120;
Integer b = 120;
System.out.println(a == b);//true
Integer a = 129;
Integer b = 129;
System.out.println(a == b);//false
为什么129就是false呢?(这里使用了==来判断,其实应该使用equals。不过这不是重点)
很明显,那就是范围问题嘛。但是,不简单是范围问题,看到最后缓存数组就明白了
首先不管是隐式还是显式,都是通过valueOf
来进行操作,那我们打开看看
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这里判断了一个low
和high
,我们随便点一个进去看
static final int low = -128;
static final int high;
原来如此,low
赋值了-128,那high
呢?
int的取值范围是-2^31^ — 2^31^-1。Integer的取值范围其实和int一样。但是对于Integer,Java为了提高效率,初始化成了-128 — 127。因此Integer取值范围在-128 — 127时效率最高。
为什么这么说呢?所以说没事干就看看源码,看源码有很多好处
首先判断i是不是在-128 — 127范围内,如果是的话,注意源码中返回了这样一个东西
return IntegerCache.cache[i + (-IntegerCache.low)];
源码虽然是英语但也很容易直观的理解
cache
,缓存,在这里意思是其实原本有一个已经准备好的数组,因为只能是数组
猜想范围
那这里就是调用了一个缓存数组用来提高效率呗,他的范围是多少?
我们代入来算一下,首先代入-128,那就是0,也就是在0下标存了-128
当i为127的时候,那就是127-(-128) = 225下标
-128 — 127这不是256个数字吗
如果i在这个范围之内,就会直接在缓存数组中取,所以就会提高效率
如果i不在这个范围之内
return new Integer(i);
源码直接返回了一个新的对象,所以才会显示false
验证范围
缓存数组的范围是前闭后闭的
[-128 — 127]
验证一下即可
3.ArrayList
List<String> list = new ArrayList<>();
之前实现过顺序表,这次就来看看Java提供的ArrayList
可以看到除了AbstractList
,ArrayList
还实现了3个接口
Seridalizable
:序列化接口,支持序列化(把对象转变为字符串。比如json,Gson)Cloneable
:就是之前讲过的克隆接口,说明是可以被克隆的嘛RandomAccess
:随机访问,支持随机访问
和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者CopyOnWriteArrayList(了解即可,后面打印有举栗)
ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
3.1 ArrayList构造方法
使用任何一个类的时候,一定要先去看一下他的构造方法
文档或者直接在idea中打开都可以
可以看到ArrayList
有3个构造方法
用哪一个都行,我们先用起来
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("hello");
没有参数的构造方法,那就是不指定初始容量嘛。默认大小为0
可以直接使用add
方法添加指定类型的内容,比如这里是字符串
一个int参数的构造方法,给容量的话那就给多少是多少嘛
ArrayList<String> arrayList1 = new ArrayList<>(10);
还有一个构造方法是利用其它Collection
构建ArrayList
意思很简单嘛,把arrayList
给arrayList2
ArrayList<String> arrayList2 = new ArrayList<>(arrayList);
用另外一个ArrayList
对新创建的ArrayList
初始化时,注意类型一定要一致
3.2 ArrayList四种打印
关于打印上一篇文章提到过
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("hello");
arrayList.add("world");
System.out.println(arrayList);
ArrayList
是没有toString
方法的,是在父类AbstractCollection
里有,所以也是可以使用的
第二种打印方式for循环遍历数组
for (int i = 0; i < arrayList.size(); i++) {
System.out.print(arrayList.get(i) + " ");
}
第三种方式那自然是foreach
for (String str : arrayList) {
System.out.print(str + " ");
}
迭代器打印
第四种,还可以使用迭代器iterator
来打印
使用其方法hasNext
来检测是否还有下一个元素
Iterator<String> it = arrayList.iterator();
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
ListIterator<String> it1 = arrayList.listIterator();
while (it1.hasNext()) {
System.out.print(it1.next() + " ");
}
可以看到这里使用了Iterator
和ListIterator
ListIterator
不仅有自己的功能还可以使用Iterator
的功能
可以再查查这2个接口,查,都可以查
add()
和remove()
这里有一点需要注意
使用迭代器的remove()
时,直接使用会报错,需要给到条件判断来防止出错。这一点面试会经常问到
必须要先迭代出所有的元素,再根据条件删除
while (it.hasNext()) {
String string = it.next();
if (string.equals("hello")) {
it.remove();
}else {
System.out.print(string + " ");
}
}
相比较于ListIterator
,Iterator
是没有add
方法的
那么我们用ListIterator
的add()
来看一下
while (it1.hasNext()) {
String string = it1.next();
if (string.equals("hello")) {
it1.add("beautiful");
}
}
System.out.println(arrayList);
原来如此,迭代器的添加比较特殊,判断到字符串hello
相等后,就直接放到了hello
后面
CopyOnWriteArrayList
现在来看看一个异常
ConcurrentModificationException
:对Vector、ArrayList在迭代的时候如果同时进行修改,就会抛出这个异常
如果使用CopyOnWriteArrayList
就可以解决这个问题,并且在迭代的同时如果使用add
方法,就会添加到末尾
CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
ArrayList不是线程安全的,CopyOnWriteArrayList是线程安全的
至于报了这样一个异常,可以在原码查看一下
modCount != expectedModCount
这2个不相等,就会报异常,至于这是个什么玩意,后面会讲
3.3 ArrayList常见方法
除了前面打印中的迭代器较为特殊,ArrayList还有其他的一些基本方法
add()
这个方法就不用多说了吧
尾插法,插入一个元素。底层是数组那默认就是插入到最后呗
add方法默认是放到数组的最后一个位置
arrayList.add("hello");
arrayList.add("world");
还可以使用add(int index, E element)
在指定位置插入元素
addAll(list)
:可以把一个list作为整体尾插
当然也可以指定位置
源码,直接点开看看嘛
在末尾size+1放入
还有指定位置index插入,先rangeCheckForAdd(index)
检查index的合法性,ensureCapacityInternal(size + 1)
,确认真实的容量。然后把其他的元素都向后移动,最后把数据插入,size++
源码的严谨和可读性确实很强,之前有写过顺序表,现在再看源码就明白中间的差距。之前写的顺序表还挺友好的
remove()
删除嘛,只可能是两种删除方式
remove(int index)
删除指定位置元素和remove(Object o)
删除第一个出现的指定元素
arrayList.remove(1);
System.out.println(arrayList);
arrayList.remove("hello");
System.out.println(arrayList);
源码,首先是删除指定位置元素的源码
检查index合法性,存储需要删除的元素,然后计算移动元素的个数,最后移动,--size最后一个元素置空。源码写的属实屌。
然后是删除第一个出现的指定元素
先判断,指定null和指定其他数据两种情况,没有要删除的元素就返回false
原来如此,还会返回一个布尔类型,接收一下看看
get()
System.out.println(arrayList.get(1));
get()
方法只有一个,获取指定下标元素
set()
set()
就是更新、设置的意思
设定指定位置元素更新为新的元素
arrayList.set(1, "测试");
System.out.println(arrayList);
clear()
清空,应该是一个一个置空,我们来看一下源码
置空,最后size = 0
arrayList.clear();
System.out.println(arrayList);
contains()
判断是否存在指定元素
System.out.println(arrayList.contains("测试"));
既然要判断,那必然是要查找一下
看源码是这样写的
返回了一个indexOf
是否大于等于0
原来如此,是让indexOf
方法查找,那么indexOf
又是什么呢
indexOf()
判断指定元素是不是null,是null的话就直接for循环去对比,不是的话就用equals进行对比,返回下标i
只要找到一个指定元素,就返回他的下标,返回的下标一定是大于等于0的,不存在就返回-1
所以indexOf()
返回第一个指定元素所在的下标
System.out.println(arrayList.indexOf("测试"));
lastindexOf()
那如果有多个指定元素,就返回最后一个的下标嘛
System.out.println(arrayList.lastIndexOf("测试"));
subList()
截取部分list,范围左闭右开
System.out.println(arrayList.subList(0, 2));
注意,subList有一些特殊,他不会将截取出来的list放到新的对象中
List<String> sub = arrayList.subList(0, 2);
sub.set(1, "world");
System.out.println(arrayList);
注意看,通过sub修改1下标的元素,原本的arrayList
就被修改了
并不是真正的截取出来
那这个如果我们要来实现,就比较麻烦了
3.4 ArrayList扩容机制
之前应该有提到过
ArrayList<String> arrayList1 = new ArrayList<>();
ArrayList<String> arrayList2 = new ArrayList<>(10);
下面arrayList2
的初始大小是指定的10,那上面arrayList1
的初始大小是多少呢?
点开后面的ArrayList
看一下
elementData
点进去会发现是数组嘛,而且是没初始化的数组
也就是说,对于顺序表来说首先是一个数组,然后还有一个size,是当前顺序表的有效数据个数
这个数组没有初始化那也就是没有大小呗,那就应该new
个对象给空间嘛
现在new
了一个ArrayList
返回来看构造方法
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
给了一个DEFAULTCAPACITY_EMPTY_ELEMENTDATA
这不也是空的嘛,这都是空的,搞什么鬼
现在new
了一个ArrayList
,底层虽然是数组,但是当前这个数组没有大小,大小为0。
那调用add()
的时候,是怎么存放的呢?为什么能存放成功没有报错,没有越界?
检测并扩容
看一下add
方法
在add()
中还有一个方法,用来检测是否需要扩容
ensureCapacityInternal(size + 1);
点开看看
要添加元素,那必然要先确定当前顺序表真正的容量吧,确定下是否需要扩容
在存放新元素的时候,size是不是为0,然后size+1也就是给这个方法传了一个1。好,现在int minCapacity = 1
nsureCapacityInternal
,在这个方法中,是不是又有两个方法ensureExplicitCapacity
和calculateCapacity
那么继续深入,把elementData
和minCapacity
代入进去,打开calculateCapacity
里面是一个判断
判断elementData
和DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是否相等,那当然相等,现在什么都没有,2个都是空嘛
既然相等,那就返回一个Math.max(DEFAULT_CAPACITY, minCapacity)
,DEFAULT_CAPACITY
和minCapacity
的最大值。
minCapacity
我们知道,我们传入的1嘛。那DEFAULT_CAPACITY
是多少呢?点开看看
原来如此,默认是个10
那作为返回值返回代入
calculateCapacity
返回最大值10,ensureExplicitCapacity
接收返回值10,看看他做了什么
modCount
modCount++;
首先,记录修改顺序表的次数
前面使用迭代器打印的时候抛出的异常,正是因为修改顺序表的次数和迭代器预期修改的次数不一样,一边添加一边打印,还有删除等等,那操作的次数不一样,就会报错了
这涉及到多线程的一个情况
ArrayList是线程不安全的。迭代器的 expectedModCount
和modCount
不相等,说明有其他线程修改了ArrayList
同样,所有使用modCount
属性的全是线程不安全的。具体深入情况和其他知识点不同,后面必定会讲,LinkedList
、HashMap
等等内部实现增删改,都有modCount
的出现。
grow
扩容
接着往下,是一个判断
if (minCapacity - elementData.length > 0)
grow(minCapacity);
minCapacity
现在还是10,减去elementData.length
,elementData
啥都没有嘛,就是0,10-0=10,大于0,就会进入下一个方法
grow
扩容方法,grow(minCapacity)
也就是grow(10)
到这一步其实就很简单了
新容量newCapacity
和旧容量oldCapacity
去比较
oldCapacity
就是elementData.length
newCapacity
通过oldCapacity
加上oldCapacity
右移得到
右移就是除以2嘛,1+1/2那不就是1.5倍扩容
但是,很可惜的是oldCapacity
现在是0
把数字代入进去可以知道第一次扩容其实大小就是10
还有一个MAX_ARRAY_SIZE
,就算是数组,那也得有一个最大值吧
如果都要比最大值大了,那么就会进入一个hugeCapacity
方法
可以看到最后返回的还是MAX_ARRAY_SIZE
。当然,一般情况也进入不到这个方法
最后分配内存
elementData = Arrays.copyOf(elementData, newCapacity);
小结
- 如果ArrayList调用不带参数的构造方法,那么初始大小是0。当第一次
add()
存放数据元素,分配大小为10。当10个放满,开始1.5倍扩容。 - 如果调用给定容量的构造方法,那么顺序表的大小为给定容量,放满后还是1.5倍扩容
扩容方法grow
扩容前会检测
使用copyOf
进行扩容
4.模拟ArrayList
实打实的模拟,使用其方法,可以更好的帮助学习理解。当然如果足够熟练,直接看源码也很好。
这次的模拟和上次可不一样了,经过前面的学习,这次的模拟不会像上次那样粗糙,当然也可能会略微粗糙一点点
4.1 字段和构造方法
把字段、构造方法等等先写出来,当然也要使用泛型
为了完全模拟可以拷贝源码,源码较为严谨完全
class MyArrayList<E> {
private Object[] elementData;
private int usedSize;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public MyArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public MyArrayList(int capacity) {
if (capacity > 0) {
this.elementData = new Object[capacity];
} else if (capacity == 0) {
this.elementData = new Object[0];
} else {
throw new IllegalArgumentException("初始化容量不能为负数");
}
}
}
4.2 add()
在前面的内容中,扩容正是从add()
开始,其中还有ensureCapacityInternal
等等函数。我们当然也要实现
检测
private void ensureCapacityInternal(int minCapacity) {
int capacity = calculateCapacity(elementData, minCapacity);
//判断,计算所需要的容量
ensureExplicitCapacity(capacity);
//扩容,使用所需要的容量
}
判断
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//1.判断之前elementDate数组是否分配过大小
return Math.max(10, minCapacity);
}
//2.分配过就返回+1后的值
return minCapacity;
}
扩容
private void ensureExplicitCapacity(int minCapacity) {
//进不去if语句说明数组还没有放满
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
//扩容
}
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) {
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
所以,实际上的add
方法,只是一个总方法
其中还需要很多其他的方法来进行操作
add()
/**
* 添加元素 尾插法
* @param e
* @return
*/
public boolean add(E e) {
ensureCapacityInternal(usedSize + 1);
this.elementData[usedSize] = e;
usedSize++;
return true;
}
int index
add()
还有插入的操作,给指定位置添加元素
那接下来就根据源码一步步模拟就ok
首先判断index
的合法性,这里要写一个size
方法,用来做比较嘛
/**
* 顺序表大小
* @return
*/
public int size() {
return this.usedSize;
}
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size()) {
throw new IndexOutOfBoundsException("指定位置不合法,插都能插歪来?");
}
}
然后还是检测容量,扩容
ensureCapacityInternal(usedSize + 1);
接下来最重要的应当是移动元素,因为要插入嘛
看一下源码是怎么做的
注意这个arraycopy
,原来是没有改动源数组,是放到了新的数组中
那我们直接写一个copy
就好了
private void copy(int index, E e) {
for (int i = usedSize - 1; i >= index; i--) {
elementData[i + 1] = elementData[i];
}
elementData[index] = e;
}
最后是重载的add
方法
public void add(int index, E e) {
rangeCheckForAdd(index);
ensureCapacityInternal(usedSize + 1);
copy(index, e);
usedSize++;
}
验证
虽然还没有写打印方法,不过调试看一下也是可以的
很好,可以正常使用
扑克牌
扑克牌的可玩性是很高的,不过这里只是简单构造一副扑克牌,只做摸牌的功能。
Card
类
很简单,扑克牌有数字和花色两部分,除了2个字段,还可以重写调用两个参数的构造方法
class Card {
private int rank;
private String suit;
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
public String toString() {
return "扑克牌【"+this.suit +
this.rank+
'】';
}
}
toString
就随便写了,现在来添加一张看看效果
public static void main(String[] args) {
Card card = new Card(3, "♥");
System.out.println(card);
}
createCards()
四种花色,首先我们先罗列出来到一个数组中
private static final String[] suits = {"♥", "♠", "♣", "♦"};
通过for循环让每一个花色都填满数字,假设没有大小王,JQK为11、12和13,那2个for循环就可以搞定
public static List<Card> createCards() {
ArrayList<Card> cards = new ArrayList<>();
for (int i = 0; i < 4; i++) {
for (int j = 1; j <= 13; j++) {
//String suit = suits[i];
//int rank = j;
//Card card = new Card(rank, suit);
//cards.add(card);
cards.add(new Card(j, suits[i]));
}
}
return cards;
}
放到ArrayList
当中也就是需要数字和花色2个参数嘛
分开写或者直接写成cards.add(new Card(j, suits[i]))
都可以
j就是数字,循环1-13添加给每个花色
洗牌
想要开始游玩这幅扑克牌,得需要洗牌吧,不洗牌那大家都挑最好的,没得意思
注意,这里有一个难点
既然要洗牌,那也就是通过两张牌的下标随机交换,那除去大小王也有52张牌0-51的下标,该怎么随机呢?
生成随机数Random
是根据给定的区间来随机,给13就0-12随机嘛
要是从头开始随机,假设先洗0下标,有可能洗过的牌再次被洗,最主要的是还有可能随机到0洗自己?不合适
那其实就可以从后开始,i从末尾开始--,和前面的j交换
int size = cards.size();//记下长度
for (int i = size - 1; i > 0; i--) {
Random random = new Random();
int rand = random.nextInt(i);
}
交换完成之后i--传给Random
随机,就不会洗到自己了,范围会逐渐缩小,洗过的牌再重复洗也就没关系了
可能会比较难理解,需要多加揣摩
然后将随机好的下标传给一个swap()
进行交换
swap(cards, i, rand);
注意,这里还有一个点
现在我们都是在面向对象,cards
可不是数组,不能直接通过下标来操作,需要get
方法
private void swap(List<Card> cards, int i, int j) {
Card tmp = cards.get(i);
cards.set(i, cards.get(j));
cards.set(j, tmp);
}
这才是真正洗好了牌
长度相等,顺序变乱
摸牌
这里简单让3个人轮流摸五张牌
那就需要给每个人都new
一个对象
首先,需要一个总对象来组织3个人
ArrayList<List<Card>> hand = new ArrayList<>();
3个人每个人都自有一个对象
List<Card> hand1 = new ArrayList<>();
List<Card> hand2 = new ArrayList<>();
List<Card> hand3 = new ArrayList<>();
将3个对象的地址传给总对象来管理
hand.add(hand1);
hand.add(hand2);
hand.add(hand3);
其实就是一个二维数组,那么使用2个for循环让3个人轮流摸五次就ok
既然每个人都要摸五次,那么外面的for循环得是5才可以
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 3; j++) {
Card card = cards.remove(0);
hand.get(j).add(card);
}
}
每次摸牌摸0下标就好了,摸走后虽然牌减少,但是0下标会顺序后移还是有牌的
j相当于是3个人,每次摸牌给自己的i
杨辉三角
杨辉三角,不陌生。现在只要使用List写一个
杨辉三角两边都是1,其他数字其实都是如下方式得来的
[i][j] = [i-1][j] + [i-1][j-1]
理解不来就画图看一下
杨辉三角的关键就在于此,上一行的当前列加上一行的前一列
首先先来创建
List<List<Integer>> lists = new ArrayList<>();
List<Integer> list1 = new ArrayList<>();
list1.add(1);
lists.add(list1);
将第一行的1添加到第一行的对象list1
中,然后将整个第一行添加给总对象lists
可以发现其实是个变相的二维数组
for (int i = 1; i < numRows; i++) {
List<Integer> list = new ArrayList<>();
list.add(1);
//每一行的开始都是1
List<Integer> preRow = lists.get(i - 1);
//上一行
for (int j = 1; j < i; j++) {
//中间
int num1 = preRow.get(j) + preRow.get(j - 1);
//上一行2个数相加
list.add(num1);
}
list.add(1);
//每一行的结尾都是1
lists.add(list);
}
return lists;
你的才华让人瞩目,期待你的更多文章。 http://www.55baobei.com/ywC0pN6M11.html
看的我热血沸腾啊www.jiwenlaw.com
不错不错,我喜欢看