java流程控制,数组,集合

37 篇文章 4 订阅
32 篇文章 1 订阅

if语句、switch语句:break 停止, 没有情况执行default。while(进入条件)循环{} 、do.. while(进入条件) 循环(至少执行一次循环体)、for循环、foreach循环. 跳转语句 break(跳出当前循环)、 continue(跳过当前循环执行下一次循环)、return (跳出方法),while的表达式的取值是 byte,short,int,char ,枚举(JDK5以后),字符串(JDK7以后)case后面只能是常量,不能是变量,且不能有相同情况。最后的一个break 可以省略,但是不建议。中途省略会case穿透,default可以放在任何位置,但是建议放在最后。跳出多层循环,可以用带标签的语句:格式 标签名:语句 ,比如 wc: for 循环。 break wc 是指跳出wc循环, 适用于多层循环。continue也可以用带标签的语句退出相应循环。当switch语句用String类型时。从本质来讲,switch对字符串的支持,其实也是int类型值的匹配。它的实现原理如下:通过对case后面的String对象调用hashCode()方法,得到一个int类型的Hash值,然后用这个Hash值来唯一标识着这个case。那么当匹配的时候,首先调用这个字符串的hashCode()方法,获取一个Hash值(int类型),用这个Hash值来匹配所有的case,如果没有匹配成功,说明不存在;如果匹配成功了,接着会调用字符串的equals()方法进行匹配。由此看出,String变量不能是null;同时,switchcase子句中使用的字符串也不能为null。

重载方法:方法名相同,参数列表不同的方法,是重载方法,与返回值无关。

数组:动态初始化:只指定数组长度,有系统为其分配初始值,静态初始化:初始化时指定每个数组元素的初始值由系统决定长度。用new关键字来分配内存,new时指定数组元素个数, 数组元素类型【】名字、 和 数组元素类型 名字【】两种形式。二维数组:数组元素类型【】【】名字 , int[][]array = new int[m][n],m代表二维数组有多少个一维数组,n代表一个一维数组的元素个数。int[][]array = new int[m][] m代表有多少个一维数组,后面的【】代表可以动态的new出不同元素个数的一位数组。静态赋值:int[][]array = {{12,0},{13,11}}。或者大括号前有两个中括号,即:int[][]array = new int [][]{{12,0},{13,11}}。 三维数组 三个大括号,每个元素为一个二维数组,以此类推。数组元素类型 名字【】【】【】和数组元素类型【】【】【】名字。栈上存放的是数组对象的引用,其他都在堆上。数组是引用类型,作为参数传入方法时,如果修改方法内副本数组的数据会体现在源数据上。但是如果方法内副本更换对象引用后修改则不会。

内存分配:栈:存放的是局部变量:在方法中或者方法声明上的变量都称为局部变量。堆:存放的是所有new出来的东西。堆内存使用完毕后变成了垃圾,担没有立即回收,等待垃圾回收器回收。栈内存数据用完就释放掉(脱离他的作用域)。数组的在栈上存储的是堆中数据的指针,跟C++一样,是第一个元素的指针,所以把一个数组直接赋值给另一个数组,改变一个数组中的元素,另一个的值也改变。

Arrays类:针对数组操作的工具类。Arrays.fill(int[]a, int value) 把指定的int值分配给int型数组的每个元素 返回值为数组。Arrays.fill(int[]a,int start(包括), int end(不包括), int value)从start索引到end索引赋值为value ,返回值维数组。这里有很多方法和重载。ArrayList:实现List接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小,比如增加和删除,注意,此实现不是同步的,再指定位置add元素的时候把指定位置和它后面的元素往后移,remove的时候把指定位置后面的元素往前移,

拆装箱:拆箱:把包装类型转换成基本类型,装箱:把基本类型转换成包装类型。比如Integer类型与10相加。针对-128到127的数据做了一个数据缓冲池,如果数据时该范围内的,每次并不创建新的空间,所以来自同一个地方byte池,所以如果两个Integer的值在数值之间的话用“==”判断两个对象是相等的。String也是一样的道理,String有一个字符串常量池。

静态导入:import导入的时候直接导到方法级别。前提:方法必须是静态的,如果有同名的静态方法,使用前必须加前缀。


可变参数:方法的参数格式:(数据类型... 变量名),调用的时候随意多个参数,0个也可以,一个方法只能有一个可变参数。这时变量实质就是一个该类型的数组,可遍历。并且方法有多个参数的时候,可变参数一定是放在最后的,因为它匹配符合条件的多个参数,Arrays工具类里的aslist方法可以把数组转化为集合, 原理就是用可变参数,但集合长度不能改变,原因是其本质还是一个数组。C#里有ref引用参数和out输出参数,再传参的时候要标注关键字,并习惯放在参数列表最后,out参数必须在方法中进行初始化,引用参数和输出参数都必须是变量,并且方法内部值得改变都会改变外面变量的值。ref和out的共性是:调用方法时复制实参变量栈中的引用(定义方法时的是形参,调用方法时传的叫实参),不同:ref要求实参必须在传递前进行赋值,out要求形参离开方法前必须赋值。通过bool表达式配合TryParse使用,防止类型转换失败。

c#:int number ; bool result = int.tryParse("520", out number); int[,] array = new in [,]{{1,2},{3,4}}array.Getlenght(0/1)获取行/列总数。

数组和链表‘:数组:查询效率快,增加删除效率低,内存连续。链表:查询效率低,增加,删除效率高,内存不连续。

集合体系:老大:Collection:分为List和Set。List分为Arraylist,LinkedList.,他们区别于Vector。Set分为HashSet和TreeSet。ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全效率高。Vector:底层数据结构是数组,查询快,增删慢,线程安全效率低。Linkedlist:底层数据结构是双向链表,查询慢,增删快,线程不安全效率高。

list接口实现类:List接口的实现类常用有ArrayList与LinkedList,ArrayList:该类实现了可变数组,允许所有元素,包裹null,可以,可以根据位置对集合进行快速访问,缺点:插入或者删除对象较慢,linkedList:采用双链链表结构保存对象,也就是说内存中是不连续的,前者时连续的。优点:便于向集合中插入和删除元素(效率高),缺点;对于随机访问集合中的对象时,效率慢。

set接口实现类:HashSet和TreeSet。由于Set集合中的对象时无序的。遍历Set集合的结果与插入Set的顺序并不相同。TreeSet是二叉树实现的,TreeSet中的数据是自动排序好的,不允许放入null值。hashSet是哈希表实现的,hashSet的数据是无序的,可以放入null,但只能放一个,两者的值都不能重复,就如数据库中唯一的约束。hashSet要求放入的对象必须实现hashCode()方法,放入的对象是以hashcode码作为标识符。而具有相同内容的string对象,hashcode是一样,所以放入的内容不饿能重复,但是同一个类的不同实例可以放入。虽然可以add同样元素,但是集合的个数不变。Treeset有两种排序方式即自然排序和自定义排序,自然排序是根据集合元素的大小以升序排列,如果定制排序应该使用Comparable接口实现。注意:Set的存储是基于map的key的存储,当我们在set里存储对象的时候,如果存储后改变对象的属性,有可能会导致在renove移除失效等问题,map是通过hash找到的key,如果对象的属性变了,hash有可能改变。

map接口实现类:HashMap和TreeMap,前者通过哈希码对其内部的映射关系快速查找,添加和删除操作效率高,TreeMap中的映射关系存在一定的顺序。hashMap无序,允许使用null值和null键,但必须保证键的唯一性,并且不保证顺序恒久不变。TreeMap不仅实现了map接口还实现了java。util。SortedMap接口,添加和删除的效率差,由于TreeMap类实现Map集合中的映射关系是根据键对象按照一定顺序排列的。因此不允许键对象是null。map的merge和compute都是针对map里的元素的put操作,merge的三个参数为key, newvalue (oldvalue, newvalue)-> 方法体 (lambda表达式:返回新的value)三个参数,如果新的值为null也会报空引用异常。compute的两个参数是key,(key, oldvalue)->{}(lambda表达式:返回新的value),但是compute的表达式里可能会存在空引用,所以如果对null操作会抛异常,这就衍生除了两个方法:computeIfAbsent和computeIfPresent,前者有两个参数,key和(key)-》{}(当key存在返回当前value值,不存在且新的值新的值不为null则执行函数并保存到map中,)后者当key存在且非空执行后面操作,两个参数为key,(key, oldvalue)(lambda返回新的值 ,如果新值为null移除key);把map的values()转换成list或者数组

    Map<String, String> map = new HashMap<String, String>();
    map.put("1", "AA");
    map.put("2", "BB");
    map.put("3", "CC");
    map.put("4", "DD");

    Collection<String> valueCollection = map.values();
    final int size = valueCollection.size();

    List<String> valueList = new ArrayList<String>(valueCollection);

    String[] valueArray = new String[size];
    map.values().toArray(valueArray);

collections(它包含对集合进行操作的多态方法)的静态synchronized...等方法返回了线程安全的集合。例如:List<String> = Collection.synchronizedList(new Arraylist<String>());

泛型定义:在定义的时候规定集合存储类型,不管是集合还是自定义类或者方法,泛型接口,所有的泛型不能用基本类型int等,只能用引用类型Integer等,集合了存储的不能是基本类型,JDK5以上存储基本类型的时候,有默认强制转换,存储后有风险,所以定义了泛型,避免了强制转换。泛型定义再接口上时放在返回类型前。定义再接口上时有两种情况,第一种在实现类的时候已经知道是什么类型了。只在实现类的实现接口后改成已知类型,第二种是定义实现类时不知道,要调用才知道,这时就要在实现类后也加上<T>并且实现接口时接口后也有<T>,eg:class A<T> 实现B<T>.这样调用的时候就能给T赋类型,也可以定义多个泛型类型,用逗号可开即可<k,v>。泛型高级通配符:1.? 任意类型,如果没有明确,可以使任何引用类型,明确写的时候前后必须一致2.? extends E  向下限定,可以使E及其子类3.? super E 向上限定, 可以使E及其父类。泛型for的应用(增强for):for(元素数组类型 变量 : 数组或者collection集合) 使用该变量就是元素。增强for就是迭代器,在里面用对象操作会报currentnodify并发修改异常。其实就是foreach,原因是因为获取集合的迭代器后,集合修改但是迭代器还是原来的所以出错。

泛型方法:public <T> void show(T t) ,可以传任意类型参数。这种方法不用吧泛型定义在类上,

Arraylist:动态数组。可以有相同的值,长度可变的容器,初始默认容量为10,此实现不同步。retainAll方法表示两个集合的交集,两个集合的交集结果放在调用方法的对象里。返回值表示调用对象是否发生改变。遍历:1.toArray方法返回object【】数组2.iterator()方法获取该集合迭代器,返回Iterator(接口)类型的子类对象,跟C#的枚举器是一样的,这里遍历可以用wihle和for循环遍历,后者只需要办判断条件改成hasnext()方法,并且对象会及时回收。Iterator定义成接口是因为,每个集合的存储方式和遍历方式不同而且必须重写,所以定义为接口,iterator()方法返回了这个集合的内部类对象,这个内部类实现了Iterator接口,并实现了对应的hasnext等方法。ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”,add时的扩容规则为“当前容量* 1.5 (如果不够就用合理的够用最小容量),当然有最大值的限制。扩容后会复制一个新的数组。remove时的操作是删除对应索引的后面元素前移。

集合里删除元素的方法:1.从小到大排序,每删除一次索引减减(i--)2.用迭代器删除,next()方法每个循环只能调一次,3.从后往前遍历删。

集合里增加元素的方法:1.用List的listIterator()迭代器遍历的时候用迭代器修改对象,此时如果添加元素,元素在迭代器元素返回的后面。2.缓存集合大小,用集合大小遍历,增加的时候手动增加缓存的size大小。

如果List<Integer> 存的是Integer元素。remove的时候参数不能是int值,需要用Integer,因为remove有两个重载方法。

public E remove(int index) {rangeCheck(index);} 通过index执行remove

public boolean remove(Object o) {}通过值执行remove

arraylist的四种初始化:

1、Arrays.asList

ArrayList<Type> obj = new ArrayList<Type>(Arrays.asList(Object o1, Object o2, Object o3, ....so on));

Arrays.asList返回的是Arrays的内部类ArrayList,并不是通常用的java.util.ArrayList。是一个长度不可变的链表.有set,get,indexOf,contains等方法,因为它继承了AbstractList,但是为实现List接口。此list的toArray()方法返回的是一个实际存储类型的数组,非Object类型数据,所以不能随便放数据。而通常Arraylist的toArray()方法返回的是Object类型数据,可以随意放数据,数组的大小为list元素的数量。

2、生成匿名内部类内进行初始化

ArrayList<T> obj = new ArrayList<T>() {{
    add(Object o1);
    add(Object o2);
    ...
    ...
}};

可以改写成

List<String> list = new ArrayList<String>() {
    // 初始化代码块 开始
    {
         this.add("one");
         this.add("two");
         this.add("three");
    }
    // 初始化代码块结束
    /*
     * 不重写任何方法
     */
};
实际上是使用一个匿名内部类继承ArrayList,而这个 匿名内部类没有重写任何方法,仅仅加入一个初始化块,并在初始化块中调用了父类的add方法向容器中加入对象

由于初始化块总是在构造器执行之前执行在匿名内部的初始化块中调用父类的add方法会不会有问题?

其实不会因为:

在创建一个Java对象时,不仅会执行该类的普通初始化块和构造器,而且系统会一直上溯到 java.lang.Object类,先执行java.lang.Object类的初始化块,开始执行java.lang.Object的构造器,依次向下执行其父类的初始化块,开始执行其父类的构造器… 最后才执行该类的初始化块和构造器,返还该类的对象。

所以说在执行匿名内部类初始化块时,父类也就是ArrayList的对象已经完成创建完成,可以任意调用其父类的方法和属性。

3、常规方式

ArrayList<T> obj = new ArrayList<T>();
obj.add("o1");
obj.add("o2");

或者

ArrayList<T> obj = new ArrayList<T>();
List list = Arrays.asList("o1","o2",...);
obj.addAll(list);

4、Collections.ncopies

ArrayList<T> obj = new ArrayList<T>(Collections.nCopies(count,element));//把element复制count次填入ArrayList中
Collections.nCopies返回的也是一个不可变的列表,为Collections的内部类CopiesList,同样继承了AbstractList,但是为实现List接口。

List:一个继承了Collection的接口,有序集合(也称为序列 )。 该界面的用户可以精确控制列表中每个元素的插入位置。 用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。listIterator()方法是List接口特有的获取迭代器的方法,有对应的往前遍历的方法和判断。不能够超出最后一个索引add。

ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常,因为Sublist是ArrayList的内部类Sublist。并不是ArrayList,而是ArrayList的一个视图,对于Sublist字列表所哟逇操作最终会反反映到原列表上,但是对原集合元素的增加和删除会改变原集合的modCount,但是不会改变子集合的modCount,而如果原集合和子结合的modCount不一致会抛出异常。所以在SubList场景中高度注意对原集合元素的增加或删除,均会导致子列表的遍历,增加,删除,set,get等操作都会产生ConcurrentModificationException异常。

arraylist的随机删除和增加操作会用到System.arraycopy()方法,实现自己到自己的复制。它属于浅复制,复制的是引用。

public static void arraycopy(Object src,
                             int srcPos,
                             Object dest,
                             int destPos,
                             int length)
src:源数组;	srcPos:源数组要复制的起始位置;
dest:目的数组;	destPos:目的数组放置的起始位置;	length:复制的长度。
注意:src and dest都必须是同类型或者可以进行转换类型的数组.
比如:
int[] fun ={0,1,2,3,4,5,6}; 
System.arraycopy(fun,0,fun,3,3);
则结果为:{0,1,2,0,1,2,6};
实现过程是这样的,先生成一个长度为length的临时数组,将fun数组中srcPos 
到srcPos+length-1之间的数据拷贝到临时数组中,再执行System.arraycopy(临时数组,0,fun,3,3).

list.subList(from, to).clear();

List接口的Object[] toArray()方法就是直接调用Arrays.copyOf(elementData, size),将list中的元素对象的引用装在一个新的生成数组中,如果是自定义的引用类型,那么修改数组中元素对象的值和列表中元素对象的值都会影响对方。String元素不会。

List接口的<T>[]T toArray(T[] a)方法会返回指定类型(必须为list元素类型的父类或本身)的数组对象,否则会报ArrayStoreException异常,如果a.length小于list元素个数就直接调用Arrays的copyOf()方法进行拷贝并且返回新数组对象,不会用参数数组,新数组中也是装的list元素对象的引用,否则先调用System.arraycopy()将list元素对象的引用装在a数组中,如果a数组还有剩余的空间,则在a[size]放置一个null,size就是list中元素的个数,这个null值可以使得toArray(T[] a)方法调用者可以判断null后面已经没有list元素了。

ArrayList中toArray()为什么不支持强制类型转换?

通常如果将ArrayList转换成array通常都是都是使用第二种方式,因为第一种方式如果进行强制类型转换会造成java.lang.ClassCastException,因为它返回的是Object[]但是Java在合法的情况下是支持父类转子类的,为什么会出现这种情况?

 public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

 public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }


 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

从源码可以看出:

Object[] toArray()对应的是(T[]) new Object[newLength],这里T就是Object

<T> T[] toArray(T[] a)对应的是(T[]) Array.newInstance(newType.getComponentType(), newLength),这里T就是String

另一方面:声明Object【】类型newObject【】类型是不能转成子类的。

        String[] arr = new String[2];
        Object[] arr2 = new Object[2];
        //arr = (String[])arr2;//java.lang.ClassCastException
        Object[] arr3 = new String[2];
        arr = (String[]) arr3;//可以转
        arr3[0] = 1;//ArrayStoreException

ArrayList的构造函数
public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

反向获取list中的指定页的索引:

public static List<Integer> getLastListRangeByPage(int page, List<Integer> arr)
	{
		final int perPage = 4;
		int allPageCount = (arr.size() + (perPage  - 1)) / perPage ;
		List<Integer> tems = new ArrayList<>();

		if (page >= 1 && page <= allPageCount)
		{
			int fromIndex = (page - 1) * perPage ;
			int toIndex = fromIndex + perPage ;
			if (toIndex > arr.size())
				toIndex = arr.size();

			fromIndex = arr.size() - fromIndex;
			toIndex = arr.size() - toIndex;

			for (int i = toIndex; i < fromIndex; ++i)
			{				
				tems.add(arr.get(i));
			}
		}
        //Collections.reverse(tem);
		return tems;
	}

Vector类型

1、这是一个集合,底层是用数组实现的,不过是可变长的,默认初始容量是10,默认增长因子是0,如果想要加入新的元素而容量不足就需要进行扩容,如果增长因子大于0,就增长负载因子个数的容量,否则增长为原来容量的两倍,如果容量仍然不够,就直接增长为所需最小容量。频繁地扩容容易引起效率问题,所以最好在调用构造函数的时候指定一个合适的容量或者调用ensureCapacity()方法进行扩容到适当的容量。它的elements()方法返回Enumeration<E>类型对象,其实就像相当于iterator()方法。

2、这个类是线程安全的,采用了快速失败机制,提供了增加、删除元素,更加方便快捷。3、线程安全不意味着对于这个容器的任何操作都是线程安全的,比如在进行迭代的时候,如果不增加一些代码保证其线程安全,其他线程是可以对这个容器做出修改的,这样也就会导致抛出ConcurrentModificationException异常。

LinkedList 类型

底层数据结构是双链表。有addFirst(),addLast(),getFirst(),removeFirst()等方法。用来实现Stack(堆栈)与Queue(队列),前者后进先出,后者是先进先出.

LinkedList的随机访问集合元素时性能较差,因为需要在双向列表从后或从前遍历中找到要index的位置,再返回元素。

在删除可插入对象和随机位置增加的动作时,为什么ArrayList的效率会比LinkedList低呢?

解析: 因为ArrayList是使用数组实现的,若要从数组中删除或插入某一个对象,需要移动后段的数组元素,从而会重新调整索引顺序,调整索引顺序会消耗一定的时间,所以速度上就会比LinkedList要慢许多. 相反,LinkedList是使用链表实现的,若要从链表中删除或插入某一个对象,只需要改变前后对象的引用即可!。

栈:先进后出 .Stack是Vector的一个子类,是通过数组实现的,它实现了一个标准的后进先出的栈。可以用双端队列代替栈。所以Java中实现栈和队列操作都可以通过使用LinkedList类实现,当然底层使用的链表。

队列:先进先出。

Set类型

不包含重复元素,无序(存储顺序和取出顺序不一致)。1.hashSet:它不保证set的迭代顺序,特别是它不保证该顺序恒久不变,不包含重复元素的原因:底层是hashmap结构(元素是链表的数组),存储的时候会比较hashcode 和地址值 和equals方法。如果不同就添加到集合中。hashcode和equals方法如果比较的对象没有重写,那么用的是object,所以自定义对象需要重写hashcode和equals方法。底层使用HashMap的key来存储元素,用一个虚拟的Object值来做hashmap的value

2.LinkedHashSet:底层是LinkedHashMap。可以同时保证数据的唯一性和有序性(存储和取出一样顺序),继承自HashSet。

3.TreeSet:底层是TreeMap(红黑数是一种自平衡的二叉树),可以用默认方法排序 ,也可以用不同构造方法提供的compara排序。不包含重复元素。不包含重复元素的原因是:第一个做为根节点存储,第二各元素开始,每个元素从根节点开始比较,大作为右子节点,小作为右子节点,相同就不管,元素取出有三种方法:前序遍历,中序遍历,后序遍历。用TreeSet存储的数据用默认自然排序需要实现compara接口。如果用自己提供的comparator,则要在初始化TreeSet时提供一个实现了comparator的类的对象。TreeSet保证元素的排序和唯一性。有firtst和last等特殊方法。

Map类型 :Map本身和Collection一样, 本身是一个顶层接口。存储键值对的元素。键是唯一的,值是可以重复的。

TreeMap:可以保证键的排序和唯一性,如果键是自定义类型,那么也要实现comparator或者创建map时给对应的实现了comparator的对象。

LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的,HashMap是单项量表。

HashMap无序;LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。linkedHashMap有序是因为它维护了一个head和tail。

 transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;

他在hashMap调用put时的new newNode()和new TreeNode()时更新双向链表元素,再new node的时候其实是new的linkedhashmap重写的Entry,它继承了HashMap的Node,初始化的时候调用了父类的构造方法,TreeNode还是父类的TreeNode。

    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

而hashMap只有一个transient Node<K,V>[] table;

LinkedHashMap存取数据,还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序。

LinkedHashMap是线程不安全的

HashMap和Hashtable的区别:前者线程不安全效率高,允许null键和null值,后者线程安全,效率低,不允许null键和null值。

HashMap可实现快速存储和检索,但其缺点是其包含的元素是无序的,这导致它在存在大量迭代的情况下表现不佳。

LinkedHashMap保留了HashMap的优势,且其包含的元素是有序的(保存了记录的插入顺序,与treeMap的键值顺序不一样)。它在有大量迭代的情况下表现更好。

TreeMap能便捷的实现对其内部元素的各种排序,但其一般性能比前两种map差。

LinkedHashMap映射减少了HashMap排序中的混乱,且不会导致TreeMap的性能损失。

JDK8的HashMap扩容 

很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。

看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率! 

所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

在JDK8里面,HashMap的底层数据结构已经变为数组+链表+红黑树的结构了,因为在hash冲突严重的情况下,链表的查询效率是O(n),所以JDK8做了优化对于单个链表的个数大于8的链表,会直接转为红黑树结构算是以空间换时间,这样以来查询的效率就变为O(logN),图示如下:

那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

TreeMap

TreeMap存储K-V键值对,通过红黑树(R-B tree)实现。

TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现。

TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;

TreeMap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过Key值的自然顺序进行排序

有firstKey()lastKey()firstEntry()lastEntry()等特殊方法。

它的descendingMap() 方法用于返回此映射中包含的映射关系的逆序视图。降序映射受此映射支持,所以映射的变化反映在降序映射中,反之亦然。它返回的是内部类DescendingSubMap对象,返回大于key(tailMap)或者小于key(headMap)的方法返回的内部类AscendingSubMap,两者都继承内部类NavigableSubMap,它跟TreeMap一样继承AbstractMap,通过tailMap和headMap和Submap处理后返回的对象如果put和remove操作时会判断isRange,是否在获取时的索引范围内,不在则抛出java.lang.IllegalArgumentException异常。

Collections:针对集合操作的工具类。而Collection 是单列集合的顶层接口。有Collections.reverse和Collections.shuffle等方法。

Map与collection区别:Map存储的是键值对形式的元素,键唯一,值可以重复。Collection存储的是单独出现的元素,子接口Set元素唯一,子接口List元素可以重复,treeSet不能存null。treeMap键不能是null。4,值可以是null。此为不处理情况。移除都为remove方法。

hashtable底层是一个单向链表为元素的数组,先通过key的hash算出index,再从数组中找出单向链表,比较hash和key来获得数据。主要是为了符号位为0.

随机掉落的算法:

https://blog.csdn.net/qq_33765907/article/details/79182355 

https://blog.csdn.net/sky_zhe/article/details/10051967

固定数量的map后加入的会替换最开始的。

    class LRUHashMap<K, V> extends LinkedHashMap<K, V>
    {

        public LRUHashMap() {
            super();
        }

        @Override
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > xx;
        }

    }

removeEldestEntry方法在put和putAll后调用,它为实施者提供每次添加新的条目时删除最老条目的机会。参数eldest为最老的映射。

SortedMap和SortedSet接口两个接口jdk1.2就已经提供,扩展的NavigableMap与NavigableSet接口jdk1.6才开始支持

从NavigableMap接口的方法中可以看出,基本上定义的都是一些边界的搜索和查询。再看下NavigableMap的定义,NavigableMap继承了SortedMap接口,而SortedMap继承了Map接口,所以NavigableMap是在Map接口的基础上丰富了这些对于边界查询的方法,但是不妨碍你只是用其中Map中自身的功能。

同样NavigableSet也是为TreeSet提供一些边界搜索方法,但是NavigableSet的一些边界搜索方法也是基于NavigableMap实现的。  NavigableSet扩展了 SortedSet,具有了为给定搜索目标报告最接近匹配项的导航方法。方法 lower、floor、ceiling 和 higher 分别返回小于、小于等于、大于等于、大于给定元素的元素,如果不存在这样的元素,则返回 null。

ConcurrentSkipListMap

ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。
ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。

在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。
但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:
1、ConcurrentSkipListMap 的key是有序的。
2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。 
在非多线程的情况下,应当尽量使用TreeMap。此外对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。对于高并发程序,应当使用ConcurrentSkipListMap,能够提供更高的并发度。

ConcurrentSkipListMap线程安全的原理与非阻塞队列ConcurrentBlockingQueue的原理一样:利用底层的插入、删除的CAS原子性操作,通过死循环不断获取最新的结点指针来保证不会出现竞态条件

所以在多线程程序中,如果需要对Map的键值进行排序时,请尽量使用ConcurrentSkipListMap,可能得到更好的并发度。
注意,调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作是个O(log(n))的操作。

说明

先以数据“7,14,21,32,37,71,85”序列为例,来对跳表进行简单说明。

跳表分为许多层(level),每一层都可以看作是数据的索引,这些索引的意义就是加快跳表查找数据速度。每一层的数据都是有序的,上一层数据是下一层数据的子集,并且第一层(level 1)包含了全部的数据;层次越高,跳跃性越大,包含的数据越少。
跳表包含一个表头,它查找数据时,是从上往下,从左往右进行查找。现在“需要找出值为32的节点”为例,来对比说明跳表和普遍的链表。

情况1:链表中查找“32”节点
路径如下图1-02所示:

需要4步(红色部分表示路径)。

情况2:跳表中查找“32”节点
路径如下图1-03所示:

忽略索引垂直线路上路径的情况下,只需要2步(红色部分表示路径)。

下面说说Java中ConcurrentSkipListMap的数据结构。
(01) ConcurrentSkipListMap继承于AbstractMap类,也就意味着它是一个哈希表。
(02) Index是ConcurrentSkipListMap的内部类,它与“跳表中的索引相对应”。Index继承于Index,ConcurrentSkipListMap中含有一个Index的对象head,head是“跳表的表头”。
(03) Index是跳表中的索引,它包含“右索引的指针(right)”,“下索引的指针(down)”和“哈希表节点node”。node是Node的对象,Node也是ConcurrentSkipListMap中的内部类。

ConcurrentSkipListMap主要用到了Node和Index两种节点的存储方式,通过volatile关键字实现了并发的操作

static final class Node<K,V> {
    final K key;
    volatile Object value;//value值       
    volatile Node<K,V> next;//next引用        
    ……
}
static class Index<K,V> {
    final Node<K,V> node;
    final Index<K,V> down;//downy引用
    volatile Index<K,V> righ                      
    ……
}

下面从ConcurrentSkipListMap的添加,删除,获取这3个方面对它进行分析

 1. 添加

实际上,put()是通过doPut()将key-value键值对添加到ConcurrentSkipListMap中的。

doPut()的源码如下:

private V doPut(K kkey, V value, boolean onlyIfAbsent) {
    Comparable<? super K> key = comparable(kkey);
    for (;;) {
        // 找到key的前继节点
        Node<K,V> b = findPredecessor(key);
        // 设置n为“key的前继节点的后继节点”,即n应该是“插入节点”的“后继节点”
        Node<K,V> n = b.next;
        for (;;) {
            if (n != null) {
                Node<K,V> f = n.next;
                // 如果两次获得的b.next不是相同的Node,就跳转到”外层for循环“,重新获得b和n后再遍历。
                if (n != b.next)
                    break;
                // v是“n的值”
                Object v = n.value;
                // 当n的值为null(意味着其它线程删除了n);此时删除b的下一个节点,然后跳转到”外层for循环“,重新获得b和n后再遍历。
                if (v == null) {               // n is deleted
                    n.helpDelete(b, f);
                    break;
                }
                // 如果其它线程删除了b;则跳转到”外层for循环“,重新获得b和n后再遍历。
                if (v == n || b.value == null) // b is deleted
                    break;
                // 比较key和n.key
                int c = key.compareTo(n.key);
                if (c > 0) {
                    b = n;
                    n = f;
                    continue;
                }
                if (c == 0) {
                    if (onlyIfAbsent || n.casValue(v, value))
                        return (V)v;
                    else
                        break; // restart if lost race to replace value
                }
                // else c < 0; fall through
            }

            // 新建节点(对应是“要插入的键值对”)
            Node<K,V> z = new Node<K,V>(kkey, value, n);
            // 设置“b的后继节点”为z
            if (!b.casNext(n, z))
                break;         // 多线程情况下,break才可能发生(其它线程对b进行了操作)
            // 随机获取一个level
            // 然后在“第1层”到“第level层”的链表中都插入新建节点
            int level = randomLevel();
            if (level > 0)
                insertIndex(z, level);
            return null;
        }
    }
}

说明:doPut() 的作用就是将键值对添加到“跳表”中。
要想搞清doPut(),首先要弄清楚它的主干部分 —— 我们先单纯的只考虑“单线程的情况下,将key-value添加到跳表中”,即忽略“多线程相关的内容”。它的流程如下:
第1步:找到“插入位置”。
即,找到“key的前继节点(b)”和“key的后继节点(n)”;key是要插入节点的键。
第2步:新建并插入节点。
即,新建节点z(key对应的节点),并将新节点z插入到“跳表”中(设置“b的后继节点为z”,“z的后继节点为n”)。
第3步:更新跳表。

2. 删除

实际上,remove()是通过doRemove()将ConcurrentSkipListMap中的key对应的键值对删除的。

doRemove()的源码如下:

final V doRemove(Object okey, Object value) {
    Comparable<? super K> key = comparable(okey);
    for (;;) {
        // 找到“key的前继节点”
        Node<K,V> b = findPredecessor(key);
        // 设置n为“b的后继节点”(即若key存在于“跳表中”,n就是key对应的节点)
        Node<K,V> n = b.next;
        for (;;) {
            if (n == null)
                return null;
            // f是“当前节点n的后继节点”
            Node<K,V> f = n.next;
            // 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
            if (n != b.next)                    // inconsistent read
                break;
            // 如果“当前节点n的值”变为null(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
            Object v = n.value;
            if (v == null) {                    // n is deleted
                n.helpDelete(b, f);
                break;
            }
            // 如果“前继节点b”被删除(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
            if (v == n || b.value == null)      // b is deleted
                break;
            int c = key.compareTo(n.key);
            if (c < 0)
                return null;
            if (c > 0) {
                b = n;
                n = f;
                continue;
            }

            // 以下是c=0的情况
            if (value != null && !value.equals(v))
                return null;
            // 设置“当前节点n”的值为null
            if (!n.casValue(v, null))
                break;
            // 设置“b的后继节点”为f
            if (!n.appendMarker(f) || !b.casNext(n, f))
                findNode(key);                  // Retry via findNode
            else {
                // 清除“跳表”中每一层的key节点
                findPredecessor(key);           // Clean index
                // 如果“表头的右索引为空”,则将“跳表的层次”-1。
                if (head.right == null)
                    tryReduceLevel();
            }
            return (V)v;
        }
    }
}

说明:doRemove()的作用是删除跳表中的节点。
和doPut()一样,我们重点看doRemove()的主干部分,了解主干部分之后,其余部分就非常容易理解了。下面是“单线程的情况下,删除跳表中键值对的步骤”:
第1步:找到“被删除节点的位置”。
即,找到“key的前继节点(b)”,“key所对应的节点(n)”,“n的后继节点f”;key是要删除节点的键。
第2步:删除节点。
即,将“key所对应的节点n”从跳表中移除 -- 将“b的后继节点”设为“f”!
第3步:更新跳表。

3. 获取

下面以get(Object key)为例,对ConcurrentSkipListMap的获取方法进行说明。get会调用doget(),doGet()是通过findNode()找到并返回节点的

private Node<K,V> findNode(Comparable<? super K> key) {
    for (;;) {
        // 找到key的前继节点
        Node<K,V> b = findPredecessor(key);
        // 设置n为“b的后继节点”(即若key存在于“跳表中”,n就是key对应的节点)
        Node<K,V> n = b.next;
        for (;;) {
            // 如果“n为null”,则跳转中不存在key对应的节点,直接返回null。
            if (n == null)
                return null;
            Node<K,V> f = n.next;
            // 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
            if (n != b.next)                // inconsistent read
                break;
            Object v = n.value;
            // 如果“当前节点n的值”变为null(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
            if (v == null) {                // n is deleted
                n.helpDelete(b, f);
                break;
            }
            if (v == n || b.value == null)  // b is deleted
                break;
            // 若n是当前节点,则返回n。
            int c = key.compareTo(n.key);
            if (c == 0)
                return n;
            // 若“节点n的key”小于“key”,则说明跳表中不存在key对应的节点,返回null
            if (c < 0)
                return null;
            // 若“节点n的key”大于“key”,则更新b和n,继续查找。
            b = n;
            n = f;
        }
    }
}

说明:findNode(key)的作用是在返回跳表中key对应的节点;存在则返回节点,不存在则返回null。
先弄清函数的主干部分,即抛开“多线程相关内容”,单纯的考虑单线程情况下,从跳表获取节点的算法。
第1步:找到“被删除节点的位置”。
根据findPredecessor()定位key所在的层次以及找到key的前继节点(b),然后找到b的后继节点n。
第2步:根据“key的前继节点(b)”和“key的前继节点的后继节点(n)”来定位“key对应的节点”。
具体是通过比较“n的键值”和“key”的大小。如果相等,则n就是所要查找的键。

实例

import java.util.*;
import java.util.concurrent.*;

/*
 *   ConcurrentSkipListMap是“线程安全”的哈希表,而TreeMap是非线程安全的。
 *
 *   下面是“多个线程同时操作并且遍历map”的示例
 *   (01) 当map是ConcurrentSkipListMap对象时,程序能正常运行。
 *   (02) 当map是TreeMap对象时,程序会产生ConcurrentModificationException异常。
 *
 * @author skywang
 */
public class ConcurrentSkipListMapDemo1 {

    // TODO: map是TreeMap对象时,程序会出错。
    //private static Map<String, String> map = new TreeMap<String, String>();
    private static Map<String, String> map = new ConcurrentSkipListMap<String, String>();
    public static void main(String[] args) {
    
        // 同时启动两个线程对map进行操作!
        new MyThread("a").start();
        new MyThread("b").start();
    }

    private static void printAll() {
        String key, value;
        Iterator iter = map.entrySet().iterator();
        while(iter.hasNext()) {
            Map.Entry entry = (Map.Entry)iter.next();
            key = (String)entry.getKey();
            value = (String)entry.getValue();
            System.out.print("("+key+", "+value+"), ");
        }
        System.out.println();
    }

    private static class MyThread extends Thread {
        MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
                int i = 0;
            while (i++ < 6) {
                // “线程名” + "序号"
                String val = Thread.currentThread().getName()+i;
                map.put(val, "0");
                // 通过“Iterator”遍历map。
                printAll();
            }
        }
    }
}

示例程序中,启动两个线程(线程a和线程b)分别对ConcurrentSkipListMap进行操作。以线程a而言,它会先获取“线程名”+“序号”,然后将该字符串作为key,将“0”作为value,插入到ConcurrentSkipListMap中;接着,遍历并输出ConcurrentSkipListMap中的全部元素。 线程b的操作和线程a一样,只不过线程b的名字和线程a的名字不同。
当map是ConcurrentSkipListMap对象时,程序能正常运行。如果将map改为TreeMap时,程序会产生ConcurrentModificationException异常。

目录 1 LINQ查询结果集 1 2 System.Array 数组 1 2.1 基于System.Array定义数组 1 2.2 基于类型定义数组 1 2.3 数组元素的清空 1 2.4 System.Array类静态成员 1 2.5 不用循环填充数组 1 2.6 数组类实例成员 2 3 System.Collections 集合 2 3.1 ArrayList 2 3.1.1 实例成员 2 3.1.2 静态成员 2 3.2 List<T> 3 3.3 Hashtable 6 3.4 SortedList 6 3.5 SortedList<TKey,TValue> 7 3.6 Queue<T> 8 3.7 Stack<T> 8 3.8 LinkedList<T> 8 3.9 HashSet<T> 9 4 System.Linq 10 4.1 System.Linq.Enumerable 10 4.2 System.Linq.Queryable 10 4.3 System.Linq.Lookup <TKey,TElement> 10 4.4 System.Linq.Expressions.Expression 10 5 接口 10 5.1 IEnumerable 、IEnumerator 10 5.1.1 正常使用 10 5.1.2 C#的 yield 12 5.2 IEnumerable <T> 12 5.3 IEnumerator <T> 12 5.4 ICollection 12 5.5 ICollection <T> 13 5.6 IList 13 5.7 IList <T> 13 5.8 IEqualityComparer 13 5.9 IEqualityComparer <T> 13 5.10 IDictionary 13 5.11 IDictionary <TKey,TValue> 13 5.12 IDictionaryEnumerator 13 5.13 IComparer 13 5.13.1 接口方法说明 int Compare(object x, object y) 13 5.13.2 ArrayList.Sort (IComparer) 方法 13 5.14 IComparer <T> 14 5.14.1 接口方法override int Compare(T x, T y)说明 14 5.14.2 List.Sort (IComparer) 方法 14 5.15 System.Linq.IGrouping<T> 14 5.16 System.Linq.ILookup<TKey,TElement> 14 5.17 System.Linq.IOrderedEnumerable<T> 14 5.18 System.Linq.IOrderedQueryable 14 5.19 System.Linq.IOrderedQueryable<T> 15 5.20 System.Linq.IQueryable 15 5.21 System.Linq.IQueryable<T> 15 5.22 System.Linq.IQueryProvider 15 6 集合扩展方法 15 6.1 集合扩展方法的实现:一个Where的例子 15 6.2 延迟类 15 6.2.1 Select 选择 16 6.2.2 SelectMany 选择 16 6.2.3 Where 条件 16 6.2.4 OrderBy 排序升 17 6.2.5 OrderByDescending 排序降 17 6.2.6 GroupBy 分组 17 6.2.7 Join 联合查询 18 6.2.8 GroupJoin 18 6.2.9 Take 获取集合的前n个元素 19 6.2.10 Skip 跳过集合的前n个元素 19 6.2.11 Distinct 过滤集合中的相同项 19 6.2.12 Union 连接不同集合,自动过滤相同项 19 6.2.13 Concat 连接不同集合,不会自动过滤相同项 19 6.2.14 Intersect 获取不同集合的相同项(交集) 20 6.2.15 Except 从某集合中删除其与另一个集合中相同的项 20 6.2.16 Reverse 反转集合 20 6.2.17 TakeWhile 条件第一次不成立就跳出循环 20 6.2.18 SkipWhile 条件第一次不成立就失效,将后面的数据全取 20 6.2.19 Cast 将集合转换为强类型集合 21 6.2.20 OfType 过滤集合中的指定类型 21 6.3 不延迟(浅复本) 21 6.3.1 Single 集合中符合条件的唯一元素,浅复本 21 6.3.2 SingleOrDefault 集合中符合条件的唯一元素(没有则返回类型默认值),浅复本 21 6.3.3 First 集合的第一个元素,浅复本 21 6.3.4 FirstOrDefault 集合中的第一个元素(没有则返回类型默认值),浅复本 22 6.3.5 Last 集合中的最后一个元素,浅复本 22 6.3.6 LastOrDefault 集合中的最后一个元素(没有则返回类型默认值),浅复本 22 6.3.7 ElementAt 集合中指定索引的元素,浅复本 22 6.3.8 ElementAtOrDefault 集合中指定索引的元素(没有则返回类型默认值),浅复本 22 6.3.9 Contains 判断集合中是否包含有某一元素 22 6.3.10 Any 判断集合中是否有元素满足某一条件 22 6.3.11 All 判断集合中是否所有元素都满足某一条件 23 6.3.12 SequenceEqual 判断两个集合内容是否相同 23 6.3.13 Count 、LongCount集合中的元素个数 23 6.3.14 Average 、Sum集合平均值求和 23 6.3.15 Max、Min 集合最大值,最小值 24 6.3.16 Aggregate 根据输入的表达式获取一个聚合值 24 6.3.17 DefaultIfEmpty 查询结果为空则返回默认值,浅复本 24 6.3.18 ToArray 将集合转换为数组,浅复本 24 6.3.19 ToList 将集合转换为List<T>集合,浅复本 25 6.3.20 ToDictionary 将集合转换为<K, V>集合,浅复本 25 7 Lambda表达式 25 7.1 例1(比效) 25 7.2 例2(多参) 27 7.3 例3(list.Where) 27 7.4 Lambda表达式中Lifting 28 8 QuerySyntax 查询语法 29 8.1 from in select 30 8.2 orderby 排序 30 8.3 group by into 分组 31 8.4 join in on equals 联合查询 33 8.5 into 汇总 33 9 DataSource 数据绑定 34
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值