黑马程序员——Java基础:集合类、泛型

 ——- android培训java培训、期待与您交流! ———-

 

一、集合框架

集合框架:又称集合类。可在API文档中查看java.util了解更多。

为什么会出现集合类?

      面向对象语言对事物的体现都是以对象的形式,所以为了方便对多个对象的操作,就对对象进行存储,集合就是存储对象最常用的一种方式。数据存储用对象,对象存储用集合。

数组和集合类同为容器,有何不同?

      数组虽然也可以存储对象,但长度是固定的;集合长度是可变的。数组中可以存储基本数据类型,集合只能存储对象。

集合的特点:

     1.只用于存储对象的容器。

     2.集合的长度是可变的。

     3.集合可以存储不同类型的对象,不可以存储基本数据类型值。

集合容器因为内部的数据结构不同,有多种具体容器。不断的向上抽取,就形成了集合框架。

集合框架的构成及分类:

二、Collection接口

Collection接口常见的方法:

1.增加:boolean add(Object obj)//添加元素

              boolean addAll(Collection coll)//将集合中的元素全部添加进该集合

2.删除:boolean remove(Object obj)//删除元素

              void clear()//清空集合

3.判断:boolean contains(Object obj)判断是否包含obj元素

              boolean containsAll(Collection coll)//判断是否包含该集合

              boolean isEmpty()//判断集合中是否有元素。

4.获取:int size()//获取集合长度

              Iterator iterator()//迭代器

5.转换:boolean retainAll(Collection coll)集合与集合取交集
              Object toArray()//将集合转成数组

Collection接口包含了两个子接口:

    |---List:元素是有序的,元素可以重复,因为该集合体系有索引。

    |---Set:元素是无序的,元素不可以重复。

List特有方法:凡是可以操作角标的方法都是该体系中的特有方法。

如:增:add(index,element)//在index角标位置上插入元素

              addAll(index,collection)//在index角标位置上添加集合

       删:remove(index)//移除角标index的元素

       改:set(index,element)//修改角标index的元素

       查:get(index)//根据角标index取元素

              subList(from,to)//从角标from到to取元素

              ListIterator()//List特有的迭代器

示例:

import java.util.*;
class ListDemo 
{
	public static void main(String[] args) 
	{
		List li=new ArrayList();
		li.add("java01");//增加
		li.add("java02");
		li.add("java03");
		li.add("java09");
		sop("增",li);
		li.remove(1);//删除
		sop("删",li);
		li.set(2,"java08");//修改
		sop("改",li);
		System.out.println("查\t"+li.get(2));//查看
		List li1=li.subList(0,li.size());
		sop("查",li1);
	}
	public static void sop(String str,List li)
	{
		Iterator it=li.iterator();//迭代器
		System.out.print(str+"\t");
		while(it.hasNext())
		{
			System.out.print(it.next()+"   ");
		}
		System.out.println();
	}
}

运行结果为:

注:这里使用的Iteratord迭代器是对所有的Collection容器进行元素取出的公共接口对象依赖于具体容器,而每一个容器的数据结构都不同,迭代器对象是在容器中进行内部实现的,也就是说iterator方法在每个容器中的实现方式是不同的。对于使用容器者而言,具体的实现不重要,只要通过容器获取到该实现的迭代器的对象即可,也就是iterator方法。

在List集合中有特有的迭代器——ListIterator,是Iterator的子接口。

在迭代时,不可以使用集合对象的方法操作集合中的元素。因为会发生异常。所以在迭代时只能用迭代器的方法操作元素,可是Iterator方法是有限的。只能对元素进行判断、取出、删除的操作。

如果想对元素进行其他操作(如:添加、修改等),就需要使用其子接口ListIterator。

示例:

import java.util.*;
class ListDemo 
{
	public static void main(String[] args) 
	{
		ArrayList al=new ArrayList();
		al.add("java01");//增加
		al.add("java02");
		al.add("java03");
		al.add("java09");
		ListIterator li;
		for(li=al.listIterator();li.hasNext();)//往后查找元素
		{
			if(li.next().equals("java03"))
			{
				li.set("java10");//修改
			}
		}
		while(li.hasPrevious())//往前查找元素
		{
			System.out.println(li.previous());
		}
	}
}

运行结果为:

List中常见的子类对象:

     |---ArrayList:底层的数据结构使用的是数组结构。特点:查询速度快,但是删除稍慢,线程不同步。

     |---LinkedList:底层使用的是链表数据结构。特点:增删速度很快,查询稍慢。

     |---Vector:底层使用的是数组数据结构。特点:线程同步,被ArrayList替代了。

 示例:

import java.util.*;
class ListDemo 
{
	public static void main(String[] args) 
	{
		Vector v=new Vector();
		v.add("java01");//增加
		v.add("java02");
		v.add("java03");
		v.add("java09");
		Enumeration en=v.elements();
		while(en.hasMoreElements())
		{
			System.out.println(en.nextElement());
		}
	}
}

运行结果为:

注:枚举(Enumeration)是Vector特有的取出方式。在这里可以发现枚举和迭代器很像,其实枚举和迭代器是一样的。因为枚举的名称过长,所以被迭代器所取代了。

LinkedList的特有方法:

 1.增加:addFirst()

        addLast()

 2.获取:获取元素,但不删除元素。如果集合中没有元素,会出现NoSuchElementException

        getFirst()

        getLast()

3.删除:获取元素,并删除元素。如果集合中没有元素,会出现NoSuchElementException

        removaFirst()

        removeLast()

JDK1.6以后,出现了替代方法。

1.增加:和上述增加方法没什么不同

       offFirst()

       offLast()

2.获取:获取元素,但是不删除。如果集合中没有元素,会返回null

        peekFirst()

       peekLast()

3.删除:获取元素,并删除元素。如果集合中没有元素,会返回null

       pollFirst()

       pollLast()

示例:用LinkedList模拟堆栈(先进后出),队列(先进先出)

import java.util.*;
class DuiLie
{
	private LinkedList ls;
	DuiLie()
	{
		ls=new LinkedList();
	}
	public void set(Object obj)
	{
		ls.addFirst(obj);
	}
	public Object get()
	{
		return ls.removeLast();//return ls.removeFirst();
	}
    public boolean isNull()
	{
		return ls.size()>0;
	}
}
class LinkedList174 
{
	public static void main(String[] args) 
	{
		DuiLie dl=new DuiLie();
		dl.set("java1");
		dl.set("java2");
		dl.set("java3");
		dl.set("java4");
		while(dl.isNull())
		{
			System.out.println(dl.get());
		}		
	}
}

运行结果为:

练习:自定义人对象作为元素存储到ArrayList集合中,并去除重复元素,如同姓名同年龄,视为同一个人。

import java.util.*;
class Person//继承自Object
{
	private String name;
	private int age;
	Person(String name,int age)
	{
		this.name=name;
		this.age=age;
	}
	public String getName()
	{
		return name;
	}
	public int getAge()
	{
		return age;
	}
	public boolean equals(Object obj)//覆盖Object中的equals方法,equals在底层自动调用的
	{
		System.out.println(this.name+"::::::"+this.age);
		if(!(obj instanceof Person))
			return false;
		Person p=(Person)obj;
		return this.name.equals(p.name)&&this.age==p.age;//这里的equals是String的方法
	}
}
class ArrayList175
{
	public static void main(String[] args)
	{
		ArrayList al=new ArrayList();
		al.add(new Person("zhangsan",24));
		al.add(new Person("lisi",49));
		al.add(new Person("zhangsan",24));
		al.add(new Person("wangwu",27));
		al.add(new Person("zhangsan",24));

		al.remove(new Person("wangwu",27));//remove的底层原理就是equals方法

		ArrayList arr=delSame(al);
		Iterator it=arr.iterator();
		while(it.hasNext())
		{
			Person p=(Person)it.next();
			System.out.println(p.getName()+";;;"+p.getAge());
		}
	}
	public static ArrayList delSame(ArrayList al)
	{
		ArrayList arr=new ArrayList();//临时容器
        Iterator li=al.iterator();//iterator()在collection接口中
		while(li.hasNext())
		{
			Object obj=li.next();
			//System.out.println(obj);
			if(!arr.contains(obj))//contains的底层原理就是equals。当boolean contains(Object o);
			                      //o==null ? e==null : o.equals(e),当不为空时会自动调用equals
				arr.add(obj);
		}
		return arr;
	}
}

运行结果为:

二、Set子接口

Set:元素是无序的(插入和取出的顺序不一定一致),元素不可以重复。

Set集合的功能和Collection是一致的。Set中常见的子类对象:

|----HashSet:底层数据结构是哈希表。线程不同步。

HashSet是如何保证元素的唯一性?

是通过元素的两个方法:hashCode和equals来完成的。如果元素的hashCode值相同,继续判断元素的equals方法是否为true如果元素的hashCode值不相同,则不会调用equals方法。

示例:自定义人对象作为元素存储到HashSet集合中,并去除重复元素,如同姓名同年龄,视为同一个人。

import java.util.*;
class Person
{
	private String name;
	private int age;
	Person(String name,int age)
	{
		this.name=name;
		this.age=age;
	}
	public int hashCode()//复写父类Object的hashCode方法
	{
		return name.hashCode()+age*12;
	}
	public boolean equals(Object obj)//复写父类Object的equals方法
	{
		if(!(obj instanceof Person))//判断对象是否为Person类型
			throw new RuntimeException("类型错误");
		Person p=(Person)obj;
		return this.name.equals(p.name)&&this.age==p.age;
	}
	public String getName()
	{
		return name;
	}
	public int getAge()
	{
		return age;
	}
}
class HashSetDemo178 
{
	public static void main(String[] args) 
	{
		HashSet hs=new HashSet();
		hs.add(new Person("zhangsan",24));
		hs.add(new Person("lisi",49));
		hs.add(new Person("zhangsan",24));
		hs.add(new Person("wangwu",27));
		hs.add(new Person("zhangsan",24));
		for(Iterator it=hs.iterator();it.hasNext();)
		{
			Person p=(Person)it.next();
			System.out.println("Name="+p.getName()+"\tAge="+p.getAge());
		}		
	}
}

运行结果为:

注意:对于判断元素是否存在,以及删除、添加等操作。依赖的方法是hashCode和equals方法。

|----TreeSet:可以对Set集合中的元素进行排序。默认按照字母的自然排序。底层数据结构是二叉树。保证元素唯一性的依据:compareTo方法return 0

TreeSet排序的第一种方式:让元素自身具备比较性,元素需要实现Compareable接口,覆盖compareTo方法,这种方式也称为元素的自然顺序,或者叫做默认顺序。

示例:按年龄排序 Comparable比较对象与对象之间的关系

import java.util.*;
class Person implements Comparable
{
	private String name;
	private int age;
	Person(String name,int age)
	{
		this.name=name;
		this.age=age;
	}
	public String getName()
	{
		return name;
	}
	public int getAge()
	{
		return age;
	}
	public int compareTo(Object obj)//复写
	{
		//System.out.println(this.name+"::::::"+this.age);
		if(!(obj instanceof Person))
			throw new RuntimeException("这不是Person对象");
		Person p=(Person)obj;
		int num=new Integer(this.age).compareTo(new Integer(p.age));
		if(num==0)
			return this.name.compareTo(p.name);
		return num;			
		//this.name.equals(p.name)&&this.age==p.age;//这里的equals是String的方法
	}
}
class TreeSet1505 
{
	public static void main(String[] args) 
	{
		TreeSet ts=new TreeSet();//
		ts.add(new Person("zhangsan",24));
		ts.add(new Person("zisi",49));
		ts.add(new Person("lisi",49));
		ts.add(new Person("zhangsan",24));
		ts.add(new Person("wangwu",27));
		ts.add(new Person("zhangsan",24));
	
		for(Iterator it=ts.iterator();it.hasNext();)
		{
			Person p =(Person)it.next();
			System.out.println(p.getName()+"......"+p.getAge());
		}
	}
}

运行结果为:

TreeSet排序的第二种方式:当元素自身不具备比较性时,或者具备的比较性不是所需要的。这时就需要让集合自身具备比较性。在集合初始化时,就有了比较方式——构造方法,即定义一个比较器,将比较器对象作为参数传递给TreeSet集合的构造函数。比较器构造方式:定义一个类,实现Comparator接口,覆盖compare方法。

示例:按字符串长度排序

import java.util.*;
class A implements Comparable
{
	public int compareTo(Object obj)
	{
		return 0;
	}
}
class B implements Comparator
{
	public int compare(Object obj1,Object obj2)
	{
	    String s1=(String)obj1;
		String s2=(String)obj2;
		//int num= s1.length()-s2.length();
		int num= new Integer(s1.length()).compareTo(new Integer(s2.length()));
		if(num==0)//长度相等时,次要条件
			return s1.compareTo(s2);
		return num;
	}	 
}
class TreeSet1505 
{
	public static void main(String[] args) 
	{
		TreeSet ts=new TreeSet(new B());//比较器对象作为参数传递给TreeSet集合的构造函数。
		ts.add("asjjjsddd");
		ts.add("zasdd");
		ts.add("vasdddd");
		ts.add("sd");
		ts.add("asd");
		ts.add("dasdd");

		for(Iterator it=ts.iterator();it.hasNext();)
		{
			Object o=it.next();
			System.out.println(o);
		}
	}
}
<span style="font-size:14px;">
</span>

运行结果为:

注:当两种排序都存在时,以比较器为主。

三、泛型

在以上示例中发现运行结果中会报的安全问题。JDK1.5版本后出现了新特性——泛型,用于解决这类安全问题,是一个类型安全机制。

好处:

     1.将运行时期出现的问题ClassCastException,转移到了编译时期。方便于程序员解决问题。让运行时期问题减少、安全。   

     2.避免了强制转换的麻烦。如在实现某一个接口时,指定传入接口方法的实参的类型的话,在复写该接口方法时就可以直接使用指定类型,而不需要强制转换。

格式:

     通过< >来定义要操作的引用数据类型。

在使用Java提供的对象时,什么时候写泛型呢?

     通常在集合框架中常见,只要见到<>就定义泛型,其实<>就是来接收类型的。当使用集合时,将集合中需要存储的数据类型作为参数传递到<>即可。注:只有引用类型才能作为泛型方法的实际参数。

泛型类:当类中要操作的引用数据类型不确定的时候,可定义泛型类。早期定义Object来完成扩展,现在定义泛型来完成扩展。

示例:自定义泛型类

class FanXing<QQ>//泛型类
{
	private QQ q;
	public void setObject(QQ q)
	{
		this.q=q;
	}
	public QQ getObject()
	{
		return q;
	}
}
class Worker
{
	private String name;
	private int age;
	Worker(String name,int age)
	{
		this.name=name;
		this.age=age;
	}
	public String getName()
	{
		return name;
	}
	public int getAge()
	{
		return age;
	}
}
class FanXingDemo1507 
{
	public static void main(String[] args) 
	{
		FanXing<Worker> fx=new FanXing<Worker>();
		fx.setObject(new Worker("zhangsan",12));
		Worker wo=fx.getObject();
		System.out.println("Name="+wo.getName()+"\tAge="+wo.getAge());
	}
}
<span style="font-size:14px;">
</span>

运行结果为:

泛型方法:泛型类定义的泛型,在整个类中有效,如果被方法使用,那么泛型类对象明确要操作的具体数据类型后,所有要操作的类型就已经固定了。即泛型类中明确好具体类型后,整个泛型类中的所以方法都是这个类型。

为了让不同方法可以操作不同类型,而且类型不确定,那么可将泛型定义在方法上。

如:class Demo

       {

            public <T> void show(T t) {}  

            public <E> void print(E t){}  

       }

注:静态方法不可以访问类上定义的泛型。如果静态方法操作的应用数据类型不确定,可以将泛型定义在方法上。

如:class Demo<T>//类型参数通常用单个大写字母表示

       {

            public static<T> void show(T t) {}  

       }

       class Demo

       {

            public static<E,F> void print(){}  //在泛型中可同时有多个类型参数,在定义它们的<>中用逗号分开

       }

泛型定义在接口上

interface Inter<T>
{
	void show(T t);
}
class InterImp<T> implements Inter<T>
{
	public void show(T t)
	{
		System.out.println(t);
	}
}
class FanXingDemo1507 
{
	public static void main(String[] args) 
	{
		Inter<Integer> i=new InterImp<Integer>();
		i.show(4);
	}
}

运行结果为:

泛型限定

     1)?:通配符,也可以理解为占位符。

     2)?extends E:可接收E类型或E类型的子类型;称之为上限。

           如:ArrayList<? extends Number>x = new ArrayList<Integer>();

     3)?super E:可接收E类型或E类型的父类型;称之为下限。

           如:ArrayList<? super Integer>x = new ArrayList<Number>();

注:除了在应用泛型时可以使用extends限定符,在定义泛型时也可以使用extends限定符。例如,Class.getAnnotation()方法的定义。并且可以用&来指定多个边界,如<V extends Serializable& cloneable> void method(){}。 

四、Map集合

定义:该集合存储的是键值对,且要保证键的唯一性。

常见方法:

1.添加:

     put(K key,V value)添加元素,如果出现添加时,相同的键,那么后添加的值会覆盖原有键对应值,并put方法会返回被覆盖的值。

      putAll(Map <? extends K,? extends V> m):添加一个集合。

2.删除:

      clear():清空

      remove(Object key):删除指定键值对

3.判断:

     containsValue(Object value):判断值是否存在

     containsKey(Object key):判断键是否存在

      isEmpty():判断是否为空

4.获取:

      get(Object key):通过键获取对应的值,可通过返回值判断键是否存在,如null为不存在。

      size():获取集合的长度

      Collection<V> value():获取Map集合中所有的值,返回一个Collection集合。

      Set<Map.Entry<K,V>> entrySet():将Map中的映射关系存入到Set集合中,而这个关系的数据类型就是Map.Entry

      Set<K>  keySet():将Map中的所有的键存入到Set集合中,因为Set具备迭代器,所以可以用迭代的方式取出所有的键,再根据get方法,获取每一个键对应的值。

Map常用的子类对象:

     |---Hashtable:底层是哈希表数据结构,不可以存入null键null值。该集合是线程同步的。JDK1.0,效率低。

     |---HashMap:底层是哈希表数据结构。允许使用null键null值,该集合是不同步的。JDK1.2,效率高。

     |---TreeMap:底层是二叉树数据结构。线程不同步。可以用于给Map集合中的键进行排序。

注:Set底层就是使用了Map集合。

Map集合取出元素的原理:将Map集合转成Set集合,再通过迭代器取出。

方式一:将Map中的映射关系存入到Set集合中,再通过迭代器取出。使用Set<Map.Entry<K,V>> entrySet()。

示例:

/*需求:
每个学生都有对应的归属地。学生student,地址String
学生属性:姓名,年龄
如姓名和年龄相同视为同一个学生,保证学生的唯一性
思路:
1描述学生
2定义map容器,将学生作为键,地址作为值,存入
3获取map集合中的元素
*/
import java.util.*;
class Student 
{
	private String name;
	private int age;
        Student(String name,int age)
	{
		this.name=name;
		this.age=age;
	}
	public String getName()
	{
		return name;
	}
	public int getAge()
	{
		return age;
	}
	public int hashCode()
	{
		//System.out.println("ff:"+(this.name.hashCode()+age*27)+"::"+(name.hashCode()));
		return this.name.hashCode()+age*27;
	}
	public boolean equals(Object obj)
	{
		if(!(obj instanceof Student))
			//return false;
		     throw new ClassCastException("类型错误");
		Student s=(Student)obj;
		//System.out.println("dd:"+s.name);
		return this.name.equals(s.name) && (this.age==s.age);
	}
}
class MapTest1519 
{
	public static void main(String[] args) 
	{
		Map<Student,String> mp=new HashMap<Student,String>();
		mp.put(new Student("zhangsan",39),"nanjing");
	        mp.put(new Student("lisi",26),"beijing");
		mp.put(new Student("zhaoliu",30),"nanjing");
		mp.put(new Student("zhangsan",39),"shanghai");
	        mp.put(new Student("wangwu",39),"shenzhen");
		mp.put(new Student("lisi",26),"beijing");
		mp.put(new Student("zhaoliu",30),"nanjing");

		Set<Map.Entry<Student,String>> ss=mp.entrySet();
		Iterator<Map.Entry<Student,String>> it=ss.iterator();
		while(it.hasNext())
		{
			Map.Entry<Student,String> me=it.next();
			Student st=me.getKey();
			String str=me.getValue();
			System.out.println(st.getName()+"::"+st.getAge()+"::::"+str);
		}
	}
}

运行结果为:

方式二将Map中的所有的键存入到Set集合中再通过迭代器取出。使用 Set<K>  keySet()。

示例:如上示例中的Student类不变,只需改变entrySet的使用。如下:

class MapTest1519 
{
	public static void main(String[] args) 
	{
		Map<Student,String> mp=new HashMap<Student,String>();
		mp.put(new Student("zhangsan",39),"nanjing");
	        mp.put(new Student("lisi",26),"beijing");
		mp.put(new Student("zhaoliu",30),"nanjing");
		mp.put(new Student("zhangsan",39),"shanghai");
	        mp.put(new Student("wangwu",39),"shenzhen");
		mp.put(new Student("lisi",26),"beijing");
		mp.put(new Student("zhaoliu",30),"nanjing");

	        Set<Student> s=mp.keySet();
		Iterator<Student> it=s.iterator();
                while(it.hasNext())
		{
			Student key=it.next();
			String value=mp.get(key);
			System.out.println(key.getName()+"::"+key.getAge()+"::::"+value);
		}
	}
}

运行结果相同。

注:当量数据之间存在着映射关系的时候,就应该想到使用Map集合。

练习:需求:"sdfgzxcvasdfxcvdf"获取该字符串中字母出现的次数。结果为:a(1)c(2).....

/*需求:"sdfgzxcvasdfxcvdf"获取该字符串中字母出现的次数。
结果为:a(1)c(2).....

思路:1.每一个字母对应一个数,有映射关系,可用map集合
      2.将字符串转换成字符数组,因为要对每一个字母进行操作
      3.定义一个map集合,因为打印结果的字母顺序,可用treemap集合
      4.遍历字符数组,将每个字母作为键去查map集合
        如果返回null,将该字母和1存入到map集合中
        如果返回不为null,说明该字母已经在map中,并有对应次数
        那么就获取该次数并进行自增,然后将该字符和自增后的次数存到map集合中,覆盖原有键对应的值
      5.将map集合中的数据变成指定的字符串形式打印
*/
import java.util.*;
class  TreeMap1521
{
	public static void main(String[] args) 
	{
		//String str="sdfgzxcvasdfxcvdf";
		String str="adfzxfxx";
		String ss=charCount(str);
		System.out.println(ss);		
	}
	public static String charCount(String str)
	{
		char[] ch=str.toCharArray();
		Map<Character,Integer> tm=new TreeMap<Character,Integer>();
		for(int i=0;i<ch.length;i++)
		{
			Integer value=tm.get(ch[i]);//通过键来查找,判断集合中是否包含该键的值
			if(value==null)
			{
				tm.put(ch[i],1);
			}
			else
			{
				value+=1;
				tm.put(ch[i],value);
			}
		}
		//System.out.println(tm);
		StringBuilder sb=new StringBuilder();//用容器来装键值
                Set<Character> s=tm.keySet();
                Iterator<Character> it=s.iterator();
		while(it.hasNext())
		{
			Character c=it.next();
			Integer in=tm.get(c);
			sb.append(c+"("+in+")");
			//System.out.println(c+"::"+in);
		}	
		//return null;
		return sb.toString();
	}
}

运行结果为:

扩展:以上所讲的是一对一的映射关系。但是在现实生活中有很多是一对多的映射关系,我们可以通过嵌套的形式将多个映射定义到一个大的集合中,再将大的集合分级处理,形成一个体系。

如:一个学校有很多班,一个班中有很多学生。可以定义为:

       HashMap<String,HashMap<String,String>> czbk=new HashMap<String,HashMap<String,String>>();//学校

       HashMap<String,String> yureban=new HashMap<String,String>();//班级

       HashMap<String,String> jiuyeban=new HashMap<String,String>();//班级

       czbk.put("yureban",yureban);
       czbk.put("jiuyueban",jiuyeban);

       yureban.put("01","zhangsan");
       jiuyeban.put("01","wangwu");

 ——- android培训java培训、期待与您交流! ———-

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值