说明

这是一个完善了但又不完善的笔记,或许以后会更新

可以参考但请务必超越

源文件


Tools


Typora
PicGo

数据结构:List

芜湖,爽爆的第一篇文章(数据结构当理解后,其实都不难)

为了更好更方便的学习,先来简单认识下泛型,毕竟已经接触到了。深入的话就之后再说吧

1.认识泛型

在这里简单先引入一个概念,泛型

简单认识一下,了解即可,看的源码就行

泛型的意义:

  1. 自动对类型进行检查
  2. 自动对类型进行强制类型转换

泛型类型不参与类型的组成。泛型尖括号当中的内容,不参与类型的组成

举栗

还是举栗学习,先写一个简单通用的顺序表

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");

而且每次取数据的时候,都需要强制类型转换

由此可以引导出三个问题

  1. 能不能指定顺序表的类型。也就是调用者用的时候指定。
  2. 指定类型后,是不是只能放指定类型的数据?
  3. 取出数据能不能不要转换

使用泛型

前面有接触过,尖括号<>

使用一个<E>代表当前类是一个泛型类,此时的这个E本质是一个占位符

class MyArrayList<E>
    
MyArrayList<String> myArrayList = new MyArrayList();
MyArrayList<Integer> myArrayList = new MyArrayList();

这样就可以把类型作为参数传递,也就是类型参数化

面试问题

泛型是怎么编译的?

泛型是编译(时)期的一种机制,擦除机制。(Object)

这其实很好理解,我们验证一下即可

前面把类型都改成了ObjectMyArrayList添加了一个<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 基本数据类型和其包装类

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

只需要特殊记下IntegerCharacter即可,其他的都是首字母大写

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);
}

这里判断了一个lowhigh,我们随便点一个进去看

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

可以看到除了AbstractListArrayList还实现了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

意思很简单嘛,把arrayListarrayList2

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() + " ");
}

可以看到这里使用了IteratorListIterator

ListIterator不仅有自己的功能还可以使用Iterator的功能

可以再查查这2个接口,查,都可以查

add()remove()

这里有一点需要注意

使用迭代器的remove()时,直接使用会报错,需要给到条件判断来防止出错。这一点面试会经常问到

必须要先迭代出所有的元素,再根据条件删除

while (it.hasNext()) {
    String string = it.next();
    if (string.equals("hello")) {
        it.remove();
    }else {
        System.out.print(string + " ");
    }
}

相比较于ListIteratorIterator是没有add方法的

那么我们用ListIteratoradd()来看一下

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作为整体尾插

当然也可以指定位置

源码,直接点开看看嘛

image-20211204182629548

在末尾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);

image-20211206151933289

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,在这个方法中,是不是又有两个方法ensureExplicitCapacitycalculateCapacity

那么继续深入,把elementDataminCapacity代入进去,打开calculateCapacity

里面是一个判断

判断elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA是否相等,那当然相等,现在什么都没有,2个都是空嘛

既然相等,那就返回一个Math.max(DEFAULT_CAPACITY, minCapacity)DEFAULT_CAPACITYminCapacity的最大值。

minCapacity我们知道,我们传入的1嘛。那DEFAULT_CAPACITY是多少呢?点开看看

原来如此,默认是个10

那作为返回值返回代入

calculateCapacity返回最大值10,ensureExplicitCapacity接收返回值10,看看他做了什么

modCount

modCount++;

首先,记录修改顺序表的次数

前面使用迭代器打印的时候抛出的异常,正是因为修改顺序表的次数和迭代器预期修改的次数不一样,一边添加一边打印,还有删除等等,那操作的次数不一样,就会报错了

这涉及到多线程的一个情况

ArrayList是线程不安全的。迭代器的 expectedModCountmodCount不相等,说明有其他线程修改了ArrayList

同样,所有使用modCount属性的全是线程不安全的。具体深入情况和其他知识点不同,后面必定会讲,LinkedListHashMap等等内部实现增删改,都有modCount的出现。

grow扩容

接着往下,是一个判断

if (minCapacity - elementData.length > 0)
    grow(minCapacity);

minCapacity现在还是10,减去elementData.lengthelementData啥都没有嘛,就是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;

广告位招租
最后修改:2021 年 12 月 09 日 08 : 19 PM
如果觉得我的文章对你有用,请喂饱我!(理直气壮)