认识List

对List接口的认识

首先说一下:上一篇文章中编辑泛型顺序表时,构造方法写的是:

public MyArrayList() {
     this.elem = (E[])new Object[10];//顺序表容量初始化为10,作E[]的强转是为了匹配E[]的字段:elem
}

有一个问题就是:为什么不写成:

this.elem = new E[10];

因为:java中不可创建泛型数组,这是根据《Effective Java》第五章中说道的:

使用泛型的作用是使得程序在编译期可以检查出与类型相关的错误,但是如果使用了泛型数组,这种能力就会受到破坏。

其次说一下:关于Object[]强制类型转换的思考:

估计很多人人认为Object[]是其他数组类型的父类,其实不然,两者属于同级的关系。根据以上错误认识,可能会有如下代码:

public class TestDemo {
    public static void main(String[] args) {
        Object[] arr=(Object[])new String[10];
        arr[0]=10;
        arr[1]=20;
        System.out.println(arr[0]);
    }
//抛出异常 java.lang.ArrayStoreException: java.lang.Integer

用C的理解方式:String[10]是一个每个元素都是String类型的数组,假设这个数组的数组指针类型是str1*,而Object[]的数组指针类型是:str2 *,上述代码的强制类型转换,只是实现了数组指针类型的整体转换(之所以可以转换,是因为它俩属于同级别的关系),但是String[]中存放的元素是String,区别于Object,所以数组中只能放String的数据。而不是我们所期望的什么类型的数据都可以放。

总之,尽量不要使用整个数组类型的转换


什么是包装类

java中的数据类型int,double等不是对象,无法通过向上转型获取到Object提供的方法,而像String却可以,只因为String是一个对象而不是一个类型。基本数据类型由于这样的特性,导致无法参与转型,泛型,反射等过程。为了弥补这个缺陷,java提供了包装类。

可以简单理解为基本数据类型的plus版本,包装类可以提供很多便捷的方法供我们使用,注意只有基本数据类型才有对应的包装类。且除了int的包装类为Integer,char的包装类是:Character,其余基本数据类型都是首字母大写。

包装类有什么用

举例:将字符串变成整数:

public class TestDemo {
    public static void main(String[] args) {
        String ret="123";//这里不能写成“hehe"这种形式的字符串!必须是数字组成的字符串
        int a=Integer.valueOf(ret);
        System.out.println(a+1);//打印124
    }
}

从上述例子可以看出:如果像C那样去实现类型的转换,我们得面向过程去编程,而java中包装类的出现,让我们得以直接使用已经编写好的一些功能。即面向对象编程。


装包、拆包

用基本数据类型的plus版本去接收一个基本数据类型的数据,这就叫装包。反过来就是拆包。

如:

public class TestDemo {
    public static void main(String[] args) {
        Integer a=10;//装包(隐式)
        int b=a;//拆包(隐式)
        System.out.println("a="+a+",b="+b);
    }
}//打印:a=10,b=10

我们可以通过javap -c进行反汇编查看,底层到底怎么实现装包和拆包的。可以发现:

装包 拆包
Integer.valueOf() Integer.intValue

所以显示的去装包和拆包就是:

public class TestDemo {
    public static void main(String[] args) {
        Integer a=Integer.valueOf(10);//装
        int b=a.intValue();//拆
        System.out.println("a="+a+",b="+b);
    }
}

注意:装完包,比如10装包了,10就是一个对象,被a引用。

所以类似于字符串对象的比较(前面的博文介绍过,还有很多变化的形式):

一个问题:

public class TestDemo {
    public static void main(String[] args) {
        Integer a=10;
        Integer b=10;
        System.out.println(a==b);
    }
}//打印true
public class TestDemo {
    public static void main(String[] args) {
        Integer a=200;
        Integer b=200;
        System.out.println(a==b);
    }
}//打印false

明明装包的基本数据一样大,发现一个true一个false哎!

究其本质:针对装包:Integer.valueOf():

public static Integer valueOf(int i) {
     if (i >= IntegerCache.low && i <= IntegerCache.high)
         return IntegerCache.cache[i + (-IntegerCache.low)];
     return new Integer(i);
}

可以看出,装包的时候,如果不是new一个对象,只要i(要存的基本数据的值)介于一定的范围,底层就直接在默认的缓存数组当中直接拿了。其范围(值):-128127,对应的下标:0256。new一个对象的话,就和String的情况类似,如:

public class TestDemo {
    public static void main(String[] args) {
        Integer a=10;//存的值在-128-127之间
        Integer b=new Integer(10);//存的值在-128-127之间
        System.out.println(a==b);
    }
}//打印false,因为对象不同

认识ArrayList✌️

认识List_第1张图片


  1. 针对ArrayList的构造方法:
1ArrayList(int)//给定初始容量的构造
2ArrayList()//无参构造
3ArrayList(Collection<?extends E>)//已现有顺序表进行构造

2.对ArrayList的遍历:

public class TestDemo {
    public static void main(String[] args) {
        ArrayList<String> arrayList=new ArrayList<>();
        arrayList.add("hehe");
        arrayList.add("xixi");
        arrayList.add("haha");
        //遍历1:直接打印
        System.out.println(arrayList);
        System.out.println("===========");//分隔一下
        //遍历2:利用for循环,size()返回顺序表存放元素的个数,get(i)获取到顺序表下标为i的元素
        for(int i=0;i<arrayList.size();i++){
            System.out.print(arrayList.get(i)+" ");
        }
        System.out.println();
        System.out.println("===========");//分隔一下
        //遍历3:利用for-each循环
        for (String ret:arrayList) {
            System.out.print(ret+" ");
        }
        System.out.println();
        System.out.println("===========");//分隔一下
        //遍历4:使用迭代器
        Iterator<String> it=arrayList.iterator();
        while(it.hasNext()){
            String ret=it.next();
            System.out.print(ret+" ");
        }
        System.out.println();
        System.out.println("===========");//分隔一下
    }
    public static void main1(String[] args) {
        ArrayList<String> arrayList=new ArrayList<>();
        ArrayList<String> arrayList1=new ArrayList<>(10);
        //发生向上转型时的构造
        List<String> arraylist2=new ArrayList<>();
        List<String> arraylist3=new ArrayList<>(10);
        //已现有
    }
}

针对迭代器:List有其特有的迭代器(一个接口):ListIterator,此迭代器实现了Iterator接口,所以说ListIterator的功能更加丰富。举例:ListIterator有add(),remove()等方法:

public class TestDemo {
    public static void main(String[] args) {
        ArrayList<String> arrayList=new ArrayList<>();
        arrayList.add("hehe");
        arrayList.add("xixi");
        arrayList.add("haha");
        ListIterator<String> it=arrayList.listIterator();
        while(it.hasNext()){
            String ret=it.next();//迭代器it每次拿到一个元素,就会指向该位置
            if(ret.equals("hehe")){
                it.add("大威天龙");//这里是尾插到It当前所指向的元素后面,此处即:hehe后面
            }
        }
        System.out.println(arrayList);
    }
}//打印:[hehe, 大威天龙, xixi, haha]

注意:如果将it.add()改成arrayList.add();编译可以通过,运行时抛出异常了!java.util.ConcurrentModificationException

首先对此:arrayList.add(),如果执行成功,其实是在整个顺序表的后面尾插一个元素,这里不成功的原因是:ArrayList不是线程安全的(单线程),如果将Arraylist改成CopyOnWriteArrayList就发现不会把报错了!

public class TestDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();
        arrayList.add("hehe");
        arrayList.add("xixi");
        arrayList.add("haha");
        ListIterator<String> it=arrayList.listIterator();
        while(it.hasNext()){
            String ret=it.next();//迭代器it每次拿到一个元素,就会指向该位置
            if(ret.equals("hehe")){
                arrayList.add("大威天龙");//这里是尾插到It当前所指向的元素后面,此处即:hehe后面
            }
        }
        System.out.println(arrayList);
    }
}
//打印:[hehe, xixi, haha, 大威天龙]

ArrayList的常见操作:

方法 解释
boolean add(E e) 尾插e
void add(int index,E e) 在下标为index的位置插入一个e
boolean addAll(Collection c) 尾插一个已有的顺序表,注意是顺序表
E remove(int index) 获取到index位置的元素,且顺序表该位置的元素被删除
E get(int index) 获取到index位置的元素,且顺序表该位置的元素不被删除
E set(int index,E e) 在顺序表的index位置设置元素e(不是插入)
void clear() 清空顺序表
boolean contains(Object o) 查看顺序表中是否包含o
int indexOf(Object o) 返回第一个o的下标
int lastIndexOf(Object o) 从后往前,返回第一个o的下标,下标还是从前往后数的
List sublist(int from,int to) 左闭右开的区截取原List的元素生成一个新的顺序表

注意:最后一个功能截取出来的元素所组成的新的顺序表,如果我们对这个新的顺序表作一些修改,那么原来的顺序表也会被作同样的修改。并且返回的必须是一个List如:

public class TestDemo {
    public static void main(String[] args) {
        ArrayList<String> arrayList=new ArrayList<>();
        arrayList.add("hehe");
        arrayList.add("xixi");
        arrayList.add("haha");
        arrayList.add("gaga");
        List<String> ret=arrayList.subList(1,3);
        ret.set(0,"大威天龙");
        System.out.println(arrayList);
        System.out.println(ret);
    }
}//打印:
//[hehe, 大威天龙, haha, gaga]
//[大威天龙, haha]

ArrayList的扩容机制✒️

从源码的角度,剖析一下,源码是怎么实现扩容的

ArrayList<String> arrayList=new ArrayList<>();//这里调用无参构造,去查看不给初始容量的化,是这么一种情况
public ArrayList() {
     this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}//DEFAULTCAPACITY_EMPTY_ELEMENTDATA={};

我们发现,无参构造调用的构造方法给我们的结果是,将底层的数组初始化为一个空数组,接下来我们往这个数组里去放东西,那就可以进一步的观察ArrayList的扩容机制了:

arrayList.add("hehe");

进入add():

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  //理解为确保至少还有一个空位给我们放元素
    elementData[size++] = e;//上一步确保能放的下之后,我们就尾插我们要放的元素即可
    return true;
}//源码

怎么确保至少还有一个位置:(进入ensureCapacityInternal()):

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);//默认值是10
    }
    return minCapacity;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

可以看出:确保最小容量的时候,源码是先进行一个容量的计算:calculateCapacity(elementData, minCapacity)

该函数实现的功能是:如果是第一次放东西,那就初始化一个容量(较大值),反之返回minCapacity,返回值用作ensureExplicitCapacity()的参数,这个函数可以明确是否扩容,当我们所需要的最小容量已经超过当前数组的长度后,进行grow(扩容),接下来看看具体怎么扩容:

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
}

可以看出,根据所需要的最小容量,想要扩充的目标容量最原始的是1.5倍扩容。

至此我们可以自己实现一个与源码相同的顺序表(当然可以先实现只放int类型数据的)。


几道与List相关的例题

  1. 将学生放入List当中,每个学生的属性是:名字(String),班级(String),成绩(score),考试结束之后,每个学生都获得各自的成绩,请遍历List集合,将班级信息打印出来:(本题的目的,是想让大家知道,使用泛型时,不一定非得指定成内置的包装类,也可以是指定成我们自己定义的类,如下面代码中的Student)

    class Student{
        public String name;
        public String classes;
        public double score;
    
        public Student(String name, String classes, double score) {
            this.name = name;
            this.classes = classes;
            this.score = score;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", classes='" + classes + '\'' +
                    ", score=" + score +
                    '}';
        }
    }
    public class Message {
        public static void main(String[] args) {
            ArrayList<Student> arrayList=new ArrayList<>();
            arrayList.add(new Student("张三","102-1",89.9));
            arrayList.add(new Student("李四","102-2",79.9));
            arrayList.add(new Student("王五","102-3",99.9));
            System.out.println(arrayList);
        }
    }//打印:
    //[Student{name='张三', classes='102-1', score=89.9}, Student{name='李四', classes='102-2', score=79.9}, Student{name='王五', classes='102-3', score=99.9}]
    
  2. 删除第一个字符串当中出现的第二个字符串中的字符

    如:String str1=“welcome to my world”;

    ​ String str2=“come”;

    ​ 输出结果应为:wl t y wrld

    public class TestDemo {
        public static void main(String[] args) {
            ArrayList<Character> arrayList=new ArrayList<>();
            String str1="welcome to my world";
            String str2="come";
            int len=str1.length();
            for (int i = 0; i <len ; i++) {
                arrayList.add(str1.charAt(i));
            }
            ListIterator<Character> it=arrayList.listIterator();
            while(it.hasNext()){
                char ret=it.next();
                if(str2.contains(ret+"")){
                    it.remove();
                }
            }
            //System.out.println(arrayList);//[w, l,  , t,  , y,  , w, r, l, d]
            //因为要输出一个字符串
            String ans="";
            for(int i=0;i<arrayList.size();i++){
                ans=ans+arrayList.get(i);
            }
            System.out.println(ans);
        }
    }
    
  3. 有一个List当中存放的是整型的数据,要求使用Collections.sort对List进行排

    public class TestDemo {
        public static void main(String[] args) {
            ArrayList<Integer> arrayList=new ArrayList<>();
            arrayList.add(10);
            arrayList.add(20);
            arrayList.add(30);
            arrayList.add(15);
            arrayList.add(29);
            Collections.sort(arrayList);
            System.out.println(arrayList);
        }
    }//打印:[10, 15, 20, 29, 30],查看Collections的sort方法可以发现,本质还是调用了List的sort()   
    

    使用顺序表制作一副扑克牌

    买牌→洗牌→三个人斗地主(揭牌)

    class Card{
        public String suit;//花色
        public int rank;//数字
    
        public Card(String suit, int rank) {
            this.suit = suit;
            this.rank = rank;
        }
    
        @Override
        public String toString() {
            return  suit + ':' +
                     + rank ;
        }
    }
    public class PlayCard {
        private static final String[] suits={"♦","♣","♥","♠"};//花色数组
        //买牌
        private static List<Card> buyCrad(){
            ArrayList<Card> cards=new ArrayList<>();
            for (int i = 0; i <suits.length ; i++) {
                for (int j = 1; j <= 13; j++) {
                    cards.add(new Card(suits[i],j));
                }
            }
            return cards;
        }
        private static void shuffle(List<Card> cards){
            int size=cards.size();
            Random random=new Random();//生成随机起点
            for(int i=size-1;i>0;i--){
                Card tmp=cards.get(i);//最后一张牌
                //将最后一张牌与前面的某一张牌进行交换
                int rand=random.nextInt(i);//[0,i)随机取一个下标
                Card tmp1=cards.get(rand);
                cards.set(rand,tmp);
                cards.set(i,tmp1);
            }
        }
        public static void main(String[] args) {
            List<Card> cards=buyCrad();//买牌
            //System.out.println(cards);此语句仅作检查买来的牌有没有毛病
            shuffle(cards);//洗牌
            //揭牌,用集合去写的话,那就是一个二维数组,假如三个人在打牌
            //那行数就是3,每个人的牌都一样多5张,一圈一圈去拿牌,最后展示每个人的牌,比如在炸金花
            ArrayList<List<Card>> hand=new ArrayList<>();
            List<Card> hand1=new ArrayList<>();
            List<Card> hand2=new ArrayList<>();
            List<Card> hand3=new ArrayList<>();
            hand.add(hand1);
            hand.add(hand2);
            hand.add(hand3);
    
    
            for(int i=0;i<5;i++){
                for (int j = 0; j <3 ; j++) {
                    Card card=cards.remove(0);//每次都是揭最上面的牌
                    hand.get(j).add(card);
                }
            }
            //看一下每个人的牌
            System.out.println("第一个人的牌:"+hand.get(0));
            System.out.println("第二个人的牌:"+hand.get(1));
            System.out.println("第三个人的牌:"+hand.get(2));
    
        }
    }
    //打印:
    第一个人的牌:[:3,:9,:9,:4,:13]
    第二个人的牌:[:7,:2,:12,:1,:11]
    第三个人的牌:[:1,:13,:6,:4,:2]
    

    可以看出,洗过牌之后,金花没有,三个人里最大的牌就是:一对9,哈哈。

你可能感兴趣的:(算法与数据结构,list,java,数据结构)