JAVA基础第三章 面向对象进阶--多态等

3.1变量及其传递

1.变量实际是内存空间。引用型变量里存储的是引用,可以理解为对象实体的地址、指针、句柄。总之,通过这个引用我们就可以操纵这个对象。

例:

  1. MyDate m,n;  //定义两个MyDate类型的引用变量  
  2. m = new MyDate( );  //令引用变量m引用刚才newMyDate类的实例(对象)  
  3. n=m; //m的引用赋给n,而并非复制两份所引用的对象再将n指向副本。即mn指向同一个对象  
  4. n.addYear( );//因为mn指向的是同一个对象,所以不论操纵哪个引用变量,所操纵的对象其实都是同一个  

2.Java在传递参数时是值传递,即将表达式的值复制给形式参数变量。

(1)对于基本型变量,当把Java基本数据类型的变量作为入口参数传给方法时,传入的参数在方法体内变成了局部变量;这个局部变量是输入参数的一个拷贝,所有方法体内部的操作都是针对这个拷贝的操作,方法执行结束后,这个局部变量也就完成了它的使命,它不影响作为输入参数的变量。

(2)对于引用型变量:方法传值时:"引用传递";引用变量间的“=”赋值:"引用传递"。所复制的是引用而非对象实体,即复制引用后实参形参会指向同一个对象(此时因形参实参指向同一个对象。因此操作其中任意一个都能改变对象实体的属性、调用对象的方法)

3.方法的返回值类型:

(1)Void:无返回值

(2)返回基本数据类型

(3)返回引用数据类型:既可以返回对象的引用,也可以返回对象。但注意需用同类的引用变量去接收方法的返回值

例1:

  1.     //实体(返回值类型是引用类型Object)  
  2. public Object getNewObject(){  
  3.     Object x = new Object();  
  4.     return x;  
  5. }  
  6.   
  7.     //调用时:  
  8.     Object p = getNewObject(); 

例2:

  1.     //实体(返回值类型是引用类型Object)
  2. public Object getNewObject(){  
  3.     return new Object();  
  4. }  
  5.     //调用时:
  6.     Object p = getNewObject(); 

4.变量分全局变量/属性变量和局部变量

(1)属性变量:类中方法外

(2)局部变量:方法中定义或方法的形参变量

(3)差别:

①存储位置:属性变量存于堆中,局部变量存于栈中

②生命周期:即什么时候产生、什么时候消失不同。属性变量是随着对象的存在而存在,局部变量是随方法的调用与否而存在

③初始值:属性变量会被自动赋初值【数值型0,boolean型false,引用型null】、局部变量必须被显式赋值

④语法:属性变量和局部变量都可被final修饰,但属性变量属于类,可用访问修饰符、static修饰,局部变量属于方法不能被访问修饰符、static修饰

3.2父类对象与子类对象的转换【这是理解多态的基础】

1.理解这个问题有个前提,就是弄清引用变量与对象之间的关系

(1)什么是某个类的对象?对象即“new 类名( )”,所以对象本身是没有名字的

(2)虽然对象本身没有名字,但我们需要操作对象,故我们需要标识符去区分不同的对象。因此,规定用该类类型的一个变量去接收这个对象。只不过因为“new 类名( )”的返回值是引用,所以这个接收对象的变量是引用变量,这个引用变量接受的值是引用值。

(3)因此,衍生出了对象的定义格式:类名 引用变量名 = new 类名( );

(4)最终,因为对象没有名字、一个引用变量只能接收一个对象的引用,所以我们就直接称这个引用变量名为对象名。而且还硬性规定了:对所创对象的所有操作都要去操作接收其引用的引用变量。

2.子父类对象的转换中所提及的对象指的是“new 类名( )”这种实质的真正的对象,并不是指引用变量。

3.存在继承关系的父类对象和子类对象之间也可以在一定条件下相互转换着使用:

(1)子类对象可以被视为其父类的一个对象(因为子类继承了父类所有的属性和方法,所以子类对象具有并且可以访问父类的成员,但注意重名成员)

    如:一个Student对象也是一个Person对象。

(2)父类对象不能被当做其某一个子类的对象(因为“父类的成员<=子类的成员”,所以对于子类对象所具有某些属性和方法,父类对象可能并不具有。)

注:如果父类的引用变量/父类的对象引用指向的实际是子类对象,则可把这个引用变量(即我们所俗称的父类的对象)当作子类对象使用。但其实质仍是把子类对象当作子类对象使用,只不过让一个引用变量作了中介。

(3)如果一个方法的形式参数定义的是父类对象,那么调用这个方法时,可以把子类对象作为实际参数(因为“子类对象可以被视为其父类的一个对象”)

3.3.1多态及虚方法的调用

1.理解多态:

(1)一个程序中相同的名字表示不同的含义。就写一个东西,但能根据实际情况表达出不同的含义、进行不同的调用。

(2)对于同一个行为,由于所用对象的不同而导致的具体的执行操作的不同。多表现为:对于同一个引用类型(如类、接口),若使用不同的实例(即不同的对象),则执行不同操作。

(3)多态是同一个行为具有多个不同表现形式或形态的能力。多态就是对于同一个“接口”,若使用不同的实例则执行不同的操作。

注:这里的“接口”并非指interface,而是这个意思:好比父类和子类都有方法A( ),那么代码中的“A( )”就不能代表具体的方法了,只能说“A( )”是这两个同名方法的一个接口、一个中介。

(4)多态: 把子类对象伪装成父类类型。让父类类型引用变量指向子类对象,即给子类对象起一个父类类型的名字。多态对象: 起了父类类型名字的子类对象。多态对象不能访问子类特有成员;多态对象被重新定义的属性表现的是父类中的属性的值;多态对象被重写的方法表现的是子类中定义的方法;总而言之:多态对象除了重写的方法 其他和父类对象完全相同

(4)编程与现实中多态的结合来理解:现实事物经常会体现出多种形态。如学生,学生是人的一种,则一个具体的同学张三既是学生也是人,即出现两种形态。Java作为面向对象的语言,同样可以描述一个事物的多种形态。如Student类继承了Person类,一个Student的对象便既是Student,又是Person。

(5)多态的一般体现:把方法的形参设为父类的引用变量,但这个引用变量既可以指向父类的实例,也可以指向各个子类的实例(即实参可以是:父类实例、父类实例的引用、子类实例、子类实例的引用)

2.多态有两种情形:

(1)编译时多态­——重载(overload):即一个类中有多个重名的方法。在编译时,虽然方法同名,但系统能根据参数的不同去匹配到同名方法中的正确的那一个。

如对于Person类的示例p,p.sayHello(); 与 p.sayHello(“Wang”);

(2)运行时多态——子类对父类方法的方法重写(override 覆盖):方法的重写导致了同名方法的出现,在运行时,如果调用这个名字的方法,就让系统根据实际调用这个名字的方法的对象来决定具体调哪个。

①实现机制:动态绑定(dynamic binding),即能动态地根据实际的调用该方法的对象(是父类对象还是子类对象、是哪个子类对象)去决定到底调用哪个方法(父类的还是哪个子类的这个同名方法)

②也称虚方法调用(virtual method invoking)。因为在写代码的时候所写的被调用的这个方法并没有真的具体到哪个具体的方法,而只有知道具体调用它的对象是谁后才能把这个方法具体到某个实际的方法,所以系统认为写代码的时候所写的这个名字的方法是一个虚拟的方法,即称作虚方法的调用。

3.多态机制大大提高了程序的和简洁性:不必对每类对象分别写调用方法,可对所有类的对象进行通用处理

4.注意:

       Parent p1 = new Child( );

       System.out.println(p1.num);//p1是Parent类的引用变量,故只能取到父类的num变量的值

       Parent p2 = new Child( );

       System.out.println(p2.show());//虽然引用变量p2的类型是Parent类,但实际引用的是Child类的对象,所以调用的是Child类的方法

5.当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

6.例题

(1)描述:

①一共有四个类: Pet类、Cat类、Dog类、Master类(主人类)。

②Pet类做父类,Cat类和Dog类做子类。

③Master类(主人类)有feed方法,Pet类、Cat类、Dog类有eat方法,feed方法内需要调用eat方法。

(2)分析:父类需要定义抽象的eat方法,各个子类需要根据自身特点分别重写父类的eat方法。则:

①对于猫类对象的喂养,需在Master类定义一个调用猫类所重写eat方法的feed方法;

②对于狗类对象的喂养,需在Master类定义一个调用狗类所重写eat方法的feed方法。

这样呢,每新有一个类,就每需在Master类定义一个专属于这个类的feed方法(因每个类都有其所特别定义的eat方法),即Master类是冗余的,而且也是不好维护的。

那么可否只用一个feed方法,实现对所有各种类的对象的喂养呢(即需要这样一个方法,它能根据所传实参的所属的类去调用各个类的同名方法中正确的那一个)?

答:用父类引用Parent p作该方法的形参列表。这个Parent类的引用p可以指向Parent类及其子类的任一实例,即Parent p=new Child();

2.AC代码:

(1)父类Pet类

  1. //父类Pet类是抽象类,定义抽象方法eat,让每种子类去重写  
  2. public abstract class Pet {  
  3.     public abstract void eat();  

(2)子类Cat类

  1. //子类Cat类继承抽象父类后需重写父类的方法  
  2. public class Cat extends Pet {  
  3.     public void eat() {  
  4.         System.out.println("猫开始吃鱼!看起来内心毫无波澜!");  
  5.     }  

(3)子类Dog类

  1. //子类Dog类继承抽象父类后需重写父类的方法  
  2. public class Dog  extends Pet {  
  3.     public void eat() {  
  4.         System.out.println("狗开始吃骨头!非常凶猛!");  
  5.     }  
  6. }  

(4)Master类

  1. public class Master {  
  2.  //feed方法的形参是Pet类的引用变量p,而这个变量p可以接收Pet类及其子类的任何实例  
  3.  //即主人在调用feed方法时,所传实参可以是父类对象、父类对象的引用、子类对象、子类对象的引用
  1.  //运行时系统会自动根据所传实参的类来正确调用对应类的eat方法    
  1.  //这样就实现了用一个方法喂养多个对象(即让一个方法根据实参所属的类去调用对应类的同名方法)
  2.     public void feed(Pet p) {  
  3.         System.out.println("主人开始喂食!"); 
  4.          p.eat(); 
  5.     }  
  6. }  

(5)Test类

  1. public class Test {    
  2.     //检测Master类定义的feed方法是否有效
  3.     public static void main(String[] args) { 
  4.         //定义Master类对象,以供主人调用feed方法;
  5.         Master master=new Master();   
  6.         //直接定义子类对象;    
  7.         Cat c=new Cat();    
  8.         Dog d=new Dog();    
  9.      //旧版JDK传入子类对象、子类对象实例的写法:Parent p=new Child();
  10.      //新版JDK可以直接传入子类对象、子类对象实例,如下所示:
  11.         master.feed(c); //传入Cat类实例的引用,主人喂养猫,则调用Cat类的eat方法  
  12.         master.feed(c); //传入Dog类实例的引用,主人喂养狗,则调用Dog类的eat方法    
  13.         }    

(6)假使新增Pet的子类Bird类

  1. //定义Bird类并让它继承Pet  
  2. public class Bird extends Pet{  
  3.     //继承抽象的父类Pet类后,要重写父类定义的eat方法  
  4.     public void eat() {  
  5.         System.out.println("鸟在吃虫子");      
  6.     }  

(7)主人若想喂养新增的Bird类的对象,在test类新定义Bird对象后,只要往原有的feed方法里传入Bird类的实例或其引用,即可调用Bird类的eat方法。

(8)上述代码中,对于Master类定义的feed方法,定义时参数列表是“Pet p”,即参数的数据类型必须是Pet类类型的引用变量,但实际使用时,我们却把Pet类子类的引用变量作为实参传入。这种将子类实例的引用,赋给父类的引用变量,以让父类的引用变量指向子类的实例,进而把子类类型当作父类类型使用的操作,称“向上转型”。

3.3.2多态及虚方法的调用—instanof和转型

1. “上溯造型(upcasting)”的概念—把子类类型当作父类类型处理:将子类实例的引用,赋给父类的引用变量,以让父类的引用变量指向子类的实例,进而把子类类型当作父类类型使用的操作,称“向上转型”。

例:

  1. Person p = new Student(); //因为Student类是Person类的子类,所以Person类的引用变量可以引用到Student的对象上,称作上溯造型  

例:

  1. void fun(Person p){  …  }     
  2.     fun(new Student());//函数所需要的参数是Person类的对象,但只要是形参所属类(Person类)及其子类的对象,那么就都可作为参数传入方法体,称作上溯造型  

2. 所有的非static、非private、、非final方法都可以进行虚方法调用,都会自动地进行动态绑定(Java的好处在于凡是不做特殊声明的方法都会自动进行动态绑定):

对于子类重写的父类的方法,在所传入的实参的实际类型与方法声明时形参要求的参数类型不同时,运行时系统会根据调用该方法的具体实例的类型(而不是方法声明时形参要求的类型)自动地匹配到正确的方法去调用,即动态绑定机制。

简单地说:能够自动地根据具体传入的实例来决定调用哪个方法

3.多态的转型分为向上转型和向下转型:

(1)向上转型:多态本身就是向上转型过的过程

①使用格式:父类类型 变量名=new 子类类型();

②适用场景(向上转型的使用场景即多态的使用场景):定义方法参数列表时、定义方法返回值类型时、定义类的成员变量时、定义数组元素类型时,都定义为父类类型的引用变量,这样就可以传递、返回、赋值、装任意子类类型的对象。就实现了定义时定义父类类型,使用时可使用子类类型的对象。

(2)向下转型:一个已经向上转型的子类实例可以使用强制类型转换,将接受这个子类实例的父类引用变量再次转为子类的引用变量。即把传入的父类引用强制转换成子类引用去使用就是向下转型,且向下转型时只有被操作的引用变量原本就是指向被转向类型的实例时才合法。

①使用格式:子类类型 新变量名=(子类类型) 父类类型的引用变量;

②适用场景:当需要使用子类的特有功能时,一般会结合instanof做判断

4.动态类型的确定:instanof

既然我们能够传进形参类的各个子类的实例及其引用,那么就涉及到了判断所传入的引用变量到底指向的是哪个类的对象的问题(即判断引用变量所引用的对象是哪个类的)。Java提供了运算符”instanof”,即“变量 instanof 类型”,表达式的值为boolean值。当引用变量所引用的是形参本类及其子类的实例时,返回true;否则返回false。
注:Object类是所有类的父类,所以Object[ ]类型的数组能够保存各种引用类型的数据。再通过instanof的条件判断,我们就可以把传入的各种类型的数据根据其类型区分开。在区分开后,再配合强制类型转换就可以让其执行某些特殊的操作。

5.实例

(1)父类Pet类

  1. //父类Pet类是抽象类,定义抽象方法eat,让每种子类去重写  
  2. public abstract class Pet {  
  3.     public abstract void eat();  

(2)子类Dog类

  1. //子类Dog类继承抽象父类后需重写父类的方法  
  2. public class Dog  extends Pet {  
  3.     public void eat() {  
  4.         System.out.println("狗开始吃骨头!非常凶猛!");  
  5.     }  
  6. }  

(3)子类Cat类

  1. //子类Cat类继承抽象父类后需重写父类的方法  
  2. public class Cat extends Pet {  
  3.     public void eat() {  
  4.         System.out.println("猫开始吃鱼!看起来内心毫无波澜!");  
  5.     }  
  6.     //子类Cat类的方法不仅有Pet类所有子类都有的eat方法,还有其独特的方法
  7.      //例如Cat类在eat后会有一个特殊的行为(方法)
  8.     public void catSAE() {  
  9.         System.out.println("猫的特有方法已被执行");  
  10.     }  

(4)Master类

  1. public class Master {  
  2.     public void feed(Pet pet) {  
  3.         System.out.println("主人开始喂食!");  
  4.         //调用Pet的子类所都有的eat方法  
  5.         pet.eat();  
  6.           
  7.         //想调用Pet类的子类中猫类所特有的方法  
  8.         //先用instanceof 判断传入的引用变量指向的是否为Cat类的实例  
  9.         //返回真证明传入的引用指向的是Cat类的对象,只有返回真才可以执行下面的代码 
  10.         if(pet instanceof Cat) {  
  11.              //当传入的指向Cat类对象的父类引用变量去调用Cat类的特有方法,即pet.catSAE()
  12.              //时,编译器会自动将这句话修改为“((cat)pet).catSAE()
  13.              //实际上“(cat)pet”就是把传入的pet对象强制转换成了cat的对象
  14.              //即把传入的父类引用强制转换成子类引用去使用,即发生了向下转型
  15.              //向下转型时,只有被操作的引用变量原本就是指向被转向类型的实例才合法
  16.             Cat cat=(Cat)pet;  
  17.             cat.catSAE();
  18.         }  
  19.     }  
  20. }  

3.2.3多态的适用场景

1.多态使用场景1:定义方法参数列表时,定义为父类类型,这样就可以传递任意子类类型的对象

例.  cat类、dog类:eat方法、yunDong方法;饲养员类:wei方法(给狗、猫喂食物)

(1)不使用多态时:

  1. public static void main(String[] args) {  
  2.         Dog1 d1=new Dog1("哈趴狗");  
  3.         Cat1 c1=new Cat1("波斯猫");  
  4.         Demo02DuoTaiUse1  syy=new Demo02DuoTaiUse1();  
  5.         syy.wei(c1);  
  6.         syy.wei(d1);  
  7.     }  
  8.     //方法可以使用的数据:成员变量+参数列表  
  9.     private void wei(Dog1 d) {//此方法只能实现喂狗  
  10.         d.eat();  
  11.         d.yunDong();  
  12.     }  
  13.     private void wei(Cat1 d) {//此方法只能实现喂猫  
  14.         d.eat();  
  15.         d.yunDong();  
  16.     }  
  17. }  
  18. class Dog1{  
  19.     String name;  
  20.     public Dog1(String name) {  
  21.         this.name=name;  
  22.     }  
  23.     void eat() {  
  24.         System.out.println(":"+name+"正在吃骨头!");  
  25.     }  
  26.     void yunDong() {  
  27.         System.out.println(""+name+"看门!");  
  28.     }  
  29. }  
  30. class Cat1{  
  31.     String name;  
  32.     public Cat1(String name) {  
  33.         this.name=name;  
  34.     }  
  35.     void eat() {  
  36.         System.out.println(":"+name+"正在吃鱼!");  
  37.     }  
  38.     void yunDong() {  
  39.         System.out.println(""+name+"晒太阳!");  
  40.     }  

(2)不使用多态实现的缺点:

①代码复用性差:需要为每种动物写一个wei方法

②代码扩展性差:如果有新的动物类型,需要修改饲养员类,添加新的wei方法

③代码耦合度高:饲养员类中直接使用dog类

(3)使用多态时:

  1. public static void main(String[] args) {  
  2.         //5 调用方法时   传递任意子类类型的对象  
  3.         Cat2 c=new Cat2("波斯猫");  
  4.         wei(c);  
  5.         Dog2 d=new Dog2("哈趴狗");  
  6.         wei(d);  
  7.     }  
  8.     //4 定义方法时 不要把形参定义为具体的子类类型,定义为父类类型  
  9.     public static void wei(Animal2 a) {
  10.         a.eat();  
  11.         a.yunDong();  
  12.     }  
  13.   
  14. }  
  15. //1创建父类抽取子类中共同的数据和功能写成父类  
  16. abstract class Animal2{  
  17.     String name;  
  18.     abstract void eat();//eat方法方法体无法实现:信息不完整  
  19.     abstract void yunDong();  
  20. }  
  21. //2让所有子类继承父类  
  22. class Dog2 extends Animal2{  
  23.     public Dog2 (String name) {  
  24.         this.name=name;  
  25.     }  
  26.     //3根据子类的需求 实现父类的抽象方法  
  27.     void eat() {  
  28.         System.out.println(":"+name+"正在吃骨头!");  
  29.     }  
  30.     void yunDong() {  
  31.         System.out.println(""+name+"看门!");  
  32. }  
  33. }  
  34. class Cat2 extends Animal2{  
  35.     public Cat2(String name) {  
  36.         this.name=name;  
  37.     }  
  38.     void eat() {  
  39.         System.out.println(":"+name+"正在吃鱼!");  
  40.     }  
  41.     void yunDong() {  
  42.         System.out.println(""+name+"晒太阳!");  
  43.     } 

2.多态使用场景2:定义成员变量时,定义为父类类型,这样就可以赋值任意子类类型的对象

例.  设计一个台灯类Lamp,其中台灯有开灯(on)这个方法,设计两个灯泡类,其中有红灯泡(RedBulb) 和绿灯泡(GreenBulb),它们都有一个发亮的方法,请设计出一段代码可以使台灯开启、灯泡发亮

(1)不使用多态

  1. public class LianXi1_1 {  
  2. //不使用多态  
  3.     public static void main(String[] args) {  
  4.         //创建灯泡对象  
  5.         RedBulb1 r1=new RedBulb1();  
  6.         GreenBulb1 g1=new GreenBulb1();  
  7.         //创建台灯对象  
  8.         Lamp1 l1=new Lamp1();  
  9.         //给台灯对象的属性赋值  
  10.         l1.r=r1;  
  11.         l1.g=g1;  
  12.         //调用台灯对象的方法  
  13.         l1.on();  
  14.        }  
  15. }  
  16. class Lamp1{//台灯类  
  17.     //灯泡是台灯的一个数据  
  18.     RedBulb1 r;//定义成员变量记录需要安装的红灯泡  
  19.     GreenBulb1 g;//定义成员变量记录需要安装的绿灯泡  
  20.     void on() {  
  21.         if (r!=null) {r.light();}  
  22.         if (g!=null) {g.light();}  
  23.           
  24.     }  
  25. }  
  26. class RedBulb1{  
  27.     void light(){  
  28.         System.out.println("红灯泡  发光:");  
  29.     }  
  30. }  
  31. class GreenBulb1{  
  32.     void light(){  
  33.         System.out.println("绿灯泡  发光:绿");  
  34.     }  
  35. }  

(2)缺点:

①代码复用性差:有几种灯泡类型就需在台灯类中定义几个属性,且on方法中需多次if判断

②扩展性差:有新灯泡类型,需要更改Lamp类,添加新属性且修改on方法

③耦合性强

(3)使用多态

  1. public class LianXi1_2 {  
  2. //使用多态  
  3.     public static void main(String[] args) {  
  4.         //创建灯泡对象  
  5.         RedBulb r1=new RedBulb();  
  6.         GreenBulb g1=new GreenBulb();  
  7.         //创建台灯对象  
  8.         Lamp l1=new Lamp();  
  9.         //给台灯对象的属性赋值  
  10.         l1.b=r1;  
  11.         l1.b=g1;  
  12.         //调用台灯对象的方法  
  13.         l1.on();  
  14. }  
  15. class Lamp{//台灯类  
  16.     //灯泡是台灯的一个数据  
  17.     //定义父类类型的成员变量  
  18.     Buib2 b;  
  19.     void on() {  
  20.         if (b!=null) {b.light();}  
  21.     }  
  22. }  
  23. //抽取子类中共同数据和功能形成父类  
  24. abstract class Buib2{  
  25.     abstract void light();  
  26. }  
  27. //让所有子类继承父类  
  28. class RedBulb extends Buib2{  
  29.     //根据子类需求  实现父类方法  
  30.     void light() {  
  31.         System.out.println("红灯泡  发光:");  
  32.     }  
  33. }  
  34. class GreenBulb extends Buib2{  
  35.     void light() {  
  36.         System.out.println("绿灯泡  发光:绿");  
  37.     }  
  38. }  

3.多态使用场景3:定义方法返回值类型时,定义为父类类型,这样就可以返回任意子类类型的对象

例.  三种类型circle、square、rect三种类型,都有各自的属性,都有打印周长和打印面积的方法。写一个方法,传递一个int参数n,n>1则返回一个长方形对象,n<1 返回一个正方形对象,n=1 返回一个圆形对象。本题只能通过多态实现:

  1. public class LianXi1_3 {  
  2.       
  3.     public static void main(String[] args) {  
  4.           Shape s=get(1);//多态  
  5.           s.printZC();  
  6.           s.printMJ();  
  7.     }  
  8.         //4 定义返回值类型时   定义为父类类型  
  9.     public static Shape get(int n) {  
  10.         if (n>1) {return new Rect(32);}  
  11.         if (n<1) {return new Squarel(1);}  
  12.         else return new Circle(1);  
  13.     }  
  14. //抽取子类中共同数据和功能形成父类 
  15. abstract class Shape{  
  16.     public abstract void printZC();  
  17.     public abstract void printMJ();  
  18. }  
  19. //2 所有子类继承父类  
  20. class Circle extends Shape{  
  21.     double r;  
  22.     final double PI=3.14;  
  23.     public Circle(double r) {this.r=r;}  
  24.     //3 子类根据自己的需求  实现父类的方法  
  25.     public void printZC() {System.out.println(" 周长:"+2*PI*r);}  
  26.     public void printMJ() {System.out.println(" 面积:"+r*PI*r);}  
  27. }  
  28. class Squarel extends Shape {  
  29.     double bianChang;  
  30.     public Squarel (double bianChang) {this.bianChang=bianChang;}  
  31.     public void printZC() {System.out.println("正方形  周长:"+bianChang*4);}  
  32.     public void printMJ() {System.out.println("正方形 面积:"+bianChang*bianChang);}  
  33. }  
  34. class Rect extends Shape{  
  35.     double chang,kuan;  
  36.     public Rect (double chang,double kuan) {this.chang=chang;this.kuan=kuan;}  
  37.     public void printZC() {System.out.println("长方形  周长:"+2*(chang+kuan));}  
  38.     public void printMJ() {System.out.println("长方形:"+chang *kuan);}  

4.多态使用场景4:定义数组元素类型时,定义为父类类型,这样就可以接收不同种子类类型的对象

例.  创建一个数组 :装2个圆、2个长方形、2个正方形。本题只能通过多态实现:

  1. public static void createArray() {  
  2.         Circle c1=new Circle(1);  
  3.         Squarel s1=new Squarel(2);  
  4.         Rect r1=new Rect(21);  
  5.         Circle c2=new Circle(11);  
  6.         Squarel s2=new Squarel(21);  
  7.         Rect r2=new Rect(2111);  
  8.         //定义数组必须明确:元素类型+元素个数  
  9.         Shape[] arr=new Shape[] {c1,c2,s1,s2,r1,r2};//多态  
  10.     } 

5. 总结:

定义方法参数列表时、定义方法返回值类型时、定义类的成员变量时、定义数组元素类型时,都定义为父类类型,这样就可以传递、返回、赋值、存储,任意子类类型的对象。在定义时定义父类类型,使用时可以使用任一子类类型的对象。

3.3多态及非虚方法的调用

1.Java中普通的方法都是虚方法调用(即对象实例决定了调用哪个方法) ,三种非虚的方法:

(1)static方法:以方法声明时的形参类型为准(声明的是哪个类型就调用哪个类型的方法),与实例类型无关(是属于类的方法而非属于实例的)

(2)private方法:子类不能调用,不存在虚化问题

(3)final方法:子类不能重写覆盖,不存在虚化问题

注:方法不能被虚化:即方法以类型为准而不会以实例对象为准

例子:

(1)父类Shape类

  1. public class Shape {  
  2.     static void draw(){   
  3.         System.out.println("Shape Drawing");   
  4.     }  

(2)子类Circle类

  1. public class Circle extends Shape{  
  2.     static void draw(){   
  3.         System.out.println("Draw Circle");   
  4.     }  
  5. }  

(3)子类Triangle类

  1. public class Triangle extends Shape{  
  2.     static void draw(){   
  3.         System.out.println("Draw Three Lines");   
  4.     }  
  5. }  

(4)Test类:TestStaticInvoke class

  1. public class TestStaticInvoke{  
  2.     //static方法是属于类的方法,不是属于实例的方法  
  3.     //调用doStuff()时,不论传入的实参是什么类的  
  4.     //只要是合法实参,都会被转型为Shape型的变量去执行方法体  
  5.     //即无论传入static方法的实参是哪个类的,方法体中都会按照形参规定的类去执行  
  6.     static void doStuff( Shape s ){  
  7.         s.draw();  
  8.     }  
  9.       
  10.     public static void main( String [] args ){  
  11.           
  12.         //doStuff方法是static方法,不论传入指向哪类对象的实参,总是执行形参所要求的Shape类的方法  
  13.         Circle c = new Circle();  
  14.         Triangle t = new Triangle();  
  15.         Shape s = new Circle();  
  16.         doStuff(s);  //输出Shape Drawing  
  17.         doStuff(c);  //输出Shape Drawing  
  18.         doStuff(t);  //输出Shape Drawing  
  19.           
  20.         //draw方法是static方法,属于类而不属于实例,故哪个类调用它它就执行哪个类的方法,无关实例所属的类  
  21.           
  22.         //Shape类的引用变量去调用,虽然该引用变量指向的是Circle类的对象,但依然执行Shape类的方法  
  23.         Shape s2 = new Circle();  
  24.         s2.draw();  
  25.           
  26.         //Circle类的引用变量去调用,无关该引用变量指向的对象是哪类的,就执行Circle类的方法  
  27.         Circle c2 = new Circle();  
  28.         c2.draw();  
  29.     }  

2.总结:多态分为编译时多态和运行时多态,前一个是在编译时就能明确调用函数,后一个是在运行时根据对象的实际类型而不是根据其引用变量类型来动态决定调用哪一个方法,前者通过方法重载来实现属于静态绑定,后者通过方法的覆盖(重写)来实现属于动态绑定,下示例程很明显在编译期不能确定调用哪个对象的print方法,只能在运行时通过返回对象的实际类型才能明确调用方法,这个例程是运行时多态的核心内涵,要好好理解。再有向上转型的实质就是父类对象的引用变量可以引用子类对象,反之则不行。

  1. import java.util.Scanner;  
  2.   
  3. import com.pojo1.A;  
  4. import com.pojo1.B;  
  5. import com.pojo1.C;  
  6.   
  7. public class Test {  
  8.     public static void makePrint(A a) {  
  9.         a.print();  
  10.     }  
  11.       
  12.     public static A Birthobj(int i) {  
  13.         if(i==1)    return new B();  
  14.         else        return new C();  
  15.     }  
  16.       
  17.     public static void main(String[] args) {  
  18.         Scanner sr = new Scanner(System.in);  
  19.         System.out.println("Input 0 or 1");  
  20.         int a = sr.nextInt();  
  21.         A pa = Birthobj(a);  
  22.         makePrint(pa);  
  23.     }  
  24.       
  25. }  
  26.   
  27. class A {  
  28.     public void print() {  
  29.         System.out.println("AAA");  
  30.     }  
  31. }  
  32.   
  33. class B extends A{  
  34.     public void print() {  
  35.         System.out.println("BBB");  
  36.     }  
  37. }  
  38.   
  39. class C extends B {  
  40.     public void print() {  
  41.         System.out.println("CCC");  
  42.     }  

3.4对象的构造与初始化

1.对象的构造与构造方法

(1)对象的构造是通过构造方法来实现的,执行“new 类名( )”时调用构造方法。

(2)任何一个类都有构造方法。若未显式定义,则编译器加一个隐式的默认无参构造方法。

(3)抽象类虽然不能通过“new 类名( )”来创建实例的,但也有构造方法。虽然抽象类本身不能通过“new 类名( )”来调用构造方法,但抽象类的子类也一定会调用。

2.在构造方法内也可以调用本类或父类的构造方法

(1)用“this(参数列表)”调用本类的其他构造方法

(2)用“super(参数列表)”调用直接父类的构造方法

(3)用this或super时,要放在构造方法的第一条,且只能有一条。

(4)如果没有显式书写this、super语句,则编译器会自动加上super(),即自动加上调用直接父类的无参构造方法的语句。在构造任何对象的时候,实际上都会调用一系列的构造方法(因为除了Object类以外,任何类都会有父类,它们的对象的构造过程是:先构造基类对象再构造派生类对象,以此类推)【理解了构造方法的执行过程就理解了这句话的含义】

解释(4):因为在构建某对象的时候必须先令其所有父类的构造方法都得到调用,否则整个对象的构建就可能不正确。所以在构造任何一个对象的时候,必须要先把父类的构造的初始化做好后才能构造好当前的对象。好比说,当我们构造一个Student对象的时候,首先这个对象一定先是Student类的父类Person类的一个对象,所以一定会先调用父类的构造方法。

3.一个问题:

  1. class A{  
  2.   A(int a){}//在类A里面显式定义有参构造方法,则编译器不会自动添加隐式的默认无参构造方法  
  3. }  
  4.   
  5. class B extends A{  
  6.   B(String s){};//虽然B的构造方法里没有显式编写super(),但编译器会自动添加以自动调用直接父类的默认无参构造方法  

(1)问题:class B编译报错。因B构造方法内隐式执行了super()去调用其直接父类A类的默认无参构造方法,但在B的直接父类A类中,由于显式定义了有参构造方法而使得默认无参构造方法的缺失,故报错。

(2)解决办法:在父类显式添加默认无参构造方法

4.怎样在创建对象的同时完成对象的初始化(给其属性赋值): 双括号

例如:p = new Person() {{  age=18;name=“李明”;}}

注:这种方法可以解决没有相应构造函数但又需给属性赋值的对象的创建

5. static修饰匿名方法代码块,称静态匿名方法代码块

(1)static代码块只在类第一次被使用时,执行

(2)换句话说,在程序运行期间,static代码块只运行一次,虽然其执行的具体时机不确定,但总是先于实例的初始化

(3)没有static修饰的代码块称作匿名方法代码块

(4)有new时,执行顺序:static代码块 (仅在初次加载时运行一次)> 无static匿名代码块 > 构造函数

(5)代码块要少用

3.5构造方法的执行过程

1.构造方法的执行过程:

(1)调用本类及父类的构造方法,直至最高一层(Object)

(本类构造方法调用直接父类构造方法,直接父类构造方法再调用其父类的构造方法,以此类推直至最高父类Object类的构造方法)

(2)按照赋值顺序对属性赋值(虽然被赋初值的属性定义语句在构造方法的前边,但其执行晚于构造方法)

(3)执行构造方法中的其他语句

(4)总的体现出的过程:先执行父类构造方法,再为本类属性赋值,最后执行构造方法中的其它语句
例.

2.一个问题:构造方法内调用其它方法本没什么问题,但若被调用的是虚方法(非static、非private、非final的方法)时,结果如何?

(1)结果:语法上合法却易造成事实上的不合理:由于调用的是虚方法,所以在执行父类构造方法这步里调用的可能会是子类的方法,被调用的子类方法里可能会使用子类对象的成员,然而子类成员还未初始化。

(2)建议:①在构造方法里避免调用任何的非构造方法,用尽可能简单的方法使对象进入就绪状态②唯一能够安全调用的是final方法

例:根据下例中蕴含的代码的执行顺序体会

  1. class Person{  
  2.     String name="未命名"//step4.1  
  3.     int age=-1;//step4.2  
  4.     Person( String name, int age ){  
  5.         //step3:调用自己的直接父类构造方法,直至最高一层Object  
  6.         this.name=name; this.age=age; //step5  
  7.         sayHello();//Step6:在父类构造方法里调用虚方法  
  8.     }  
  9.     void sayHello(){  
  10.         System.out.println( "我是一个人,我名叫:" + name + ",年龄为:"+ age );  
  11.     }  
  12. }  
  13.   
  14. class Student extends Person{  
  15.     String school="未定学校";//Step8  
  16.     Student( String name, int age, String school ){  
  17.         super( name, age );//Step2:调用直接父类构造方法  
  18.         this.school = school;//Step9  
  19.     }  
  20.     void sayHello(){//Step7  
  21.         //Student的属性还未赋值,本方法内却使用了属性的值,会出错  
  22.         System.out.println( "我是学生,我名叫:" + name + ",年龄为:"+ age + ",学校在:" + school );  
  23.     }  
  24. }  
  25.   
  26. class ConstructInvokeMetamorph {  
  27.     public static void main(String[] args){   
  28.         Person p = new Student("李明"18"北大"); //Step1:调用子类构造方法初始化子类对象  
  29.     }  

3.6对象的清除与垃圾回收

1.对象的创建与销毁

(1)创建对象:new 类名( )

(2)销毁对象:Java中自动清除,不需要使用delete

2.对象的自动清除称为垃圾回收(garbage collection)

(1)对象回收是由Java虚拟机的垃圾回收线程来完成的(即相当于在后台自动运行)

(2)系统怎样知道对象是否为垃圾?任何对象都有一个引用计数器,当其值为0时(即对象实体空间没有被引用时),说明该对象可以回收

3.System类的static方法 System.gc( )

(1)当内存空间使用较多时,使用此方法

(2)此方法的作用是:建议虚拟机进行垃圾回收,但仅仅只是“建议”

4.Object类的方法 finalize( )

(1)在C++执行销毁对象等一系列操作的方法称“析构方法(destructor)”,虽然Java没有“析构方法”,但Object类的方法finalize()有类似功能

(2)虚拟机在垃圾回收时会自动调用对象的finalize()方法

(3)Object类的finalize()方法:

protected void finalize() throws Throwable{}   // Object类的finalize()方法实际上什么也没做

(4)但子类可以重写覆盖Object类的finalize()方法,此时就可以添加一些释放系统资源等操作的语句了。另约定:子类的finalize()方法在执行时应调用父类的finalize()方法,以保证父类的清理工作也能够正常进行。

(5)finalize( )方法很少用:在代码中,一般不去主动销毁对象,而是为了能更好的控制释放资源的过程。一方面垃圾回收由系统自动完成,很难人为控制;另一方面,finalize()方法的调用时机并不确定;所以一般不用finalize(),除非是写框架性应用程序等特殊程序时才使用

5.try-with-resources语句:是一种声明了一种或多种资源的try语句。

(1)资源是指在程序用完了之后必须要关闭的对象。try-with-resources语句保证了每个声明了的资源在语句结束的时候都会被关闭。

(2)任何实现了java.lang.AutoCloseable接口的对象,和实现了java.io.Closeable接口的对象,都可以当做资源使用。

例.

  1. try( Scanner cin = new Scanner( System.in ) ){  
  2.     //使用资源等一系列操作  
  3. }  
  4.     //表示只要执行完了花括号的语句,无论抛出正常还是异常,都会自动调用java.lang.AutoCloseable接口的close()方法,相当于  
  5. finally{  
  6.     Scanner.close();  
  7. }  

3.7.1内部类与匿名类—内部类

1.简介

(1)内部类( inner class )是在其他类中定义的类

(2)匿名类( anonymous class)是一种特殊的内部类,它没有类名。

2.内部类的定义          

(1)将类的定义class xxxx{…}置入一个类的内部即可

    (2)编译器会生成名字为xxxx$xxxx的class文件

           (3)内部类不能与外部类同名

3.内部类的使用

    (1)在封装它的类的内部使用内部类,与普通类的使用方式相同

    (2)在其它地方使用:

①类名前要冠以外部类的名字

②用new创建内部类实例时,也要在new前冠以引用外部类对象的外部类引用变量

例:外部类名.内部类名  name = 外部类对象名.new 内部类名(参数)

示例 TestInnerClass.java

  1. class TestInnerClass{  
  2.     public static void main( String[] args ){  
  3.         Parcel p = new Parcel();  
  4.         p.testShip();  
  5.   
  6.         Parcel.Contents c = p.new Contents(33);  
  7.         Parcel.Destination d = p.new Destination( "Hawii" );  
  8.     }  
  9. }  
  10.   
  11. class Parcel {  
  12.     private Contents c;  
  13.     private Destination d;  
  14.     class Contents {  
  15.         private int i;  
  16.         Contents( int i ){ this.i = i; }  
  17.         int value() { return i; }  
  18.     }  
  19.     class Destination {  
  20.         private String label;  
  21.         Destination(String whereTo) {label = whereTo;}  
  22.         String readLabel() { return label; }  
  23.     }  

4.在内部类中使用外部类的成员

(1)内部类中可以直接访问外部类的属性及方法:即使private也可

(2)如果内部类中有与外部类同名的属性或方法,则可以用:外部类名.this.字属性及方法

例:

  1. class A  
  2. {  
  3.     private int s = 111;  
  4.   
  5.     public class B {  
  6.         private int s = 222;  
  7.         public void mb(int s) {  
  8.             System.out.println(s); // 局部变量s  
  9.             System.out.println(this.s); // 内部类对象的属性s  
  10.             System.out.println(A.this.s); //  外层类对象属性s  
  11.         }  
  12.     }  

5.内部类的修饰符:内部类和内部类类中的属性、方法都是外部类的成员,它的前面也可以有访问控制符和其他修饰符。

    (1)访问控制符:public,protected,默认,private(外部类只能用public或默认)

    (2)其他修饰符:final,static,abstract

(3)static修饰符

①用static修饰内部类,表明该内部类实际上是一种外部类,与普通类基本没有区别

           ②static表明该内部类与对象无关,但一般的内部类都是与外部类的对象有关的,所以修饰后的内部类与外部类的实例也无关

③static类在使用时:

           (A)实例化static类时,new前面不需要用对象实例变量,但类名前要冠以外部类类名

           (B)static的不能访问非static的属性及方法

示例 TestInnerStatic.java

  1. class Outer   {  
  2.     static class Inner{  
  3.         //外部类Outer里面有一个static内部类Inner  
  4.     }  
  5. }  
  6.   
  7. class TestInnerStatic{  
  8.     public static void main(String[] args) {  
  9.         创建对象的时候不需要用"对象.",而是直接new,但类名前要冠以外部类类名  
  10.         Outer.Inner oi = new Outer.Inner();  
  11.         //Outer.Inner oi2 = Outer.new Inner();  //error     
  12.         //Outer.Inner oi3 = new Outer().new Inner(); // error  
  13.     }  

6.局部类很少用

(1)局部类:在方法中定义的类,称为”方法中的内部类”,或叫局部类(local class)

(2)使用局部类

①就像局部变量一样,方法中的内部类,不能用 public,private,protected,static修饰,但可被final(不可继承的)或者abstract(抽象的)修饰。

②可以访问其外部类的成员,但不能够访问该方法的非final局部变量(因为该方法的局部变量随方法的调用而存在、是变化的。但final修饰后,就不可变了)

3.7.2内部类与匿名类—匿名类(很常用)

1.匿名类( anonymous class)是一种特殊的内部类、是“一次性使用”的类、是内部类的一种简写:没有类名,在定义类的同时生成该对象的一个实例。

示例 TestInnerAnonymous.java

  1. class Outer{  
  2.     private int size = 5;  
  3.     public Object makeTheInner( int localVar ){  
  4.         final int finalLocalVar = 99;  
  5.         return new Object()  {//new对象的同时,用花括号定义了类的类体  
  6.             //到底创建哪个类的对象,父类、接口都可  
  7.             public String toString() {  
  8.                 return ( " InnerSize: " + size +   
  9.                     " finalLocalVar: " + finalLocalVar  
  10.                 );  
  11.             }  
  12.         };  
  13.     }  

2.匿名类的使用

(1)不取名字、不使用关键词class、也不使用extends及implements,直接用其父类或接口的名字:也就是说,该类要么是某父类的子类,要么是实现了某个接口的类

注:编译器会自动生成 xxxxx$1之类的名字

(2)在定义类的同时就创建实例,类体前new对象,格式:

new 父类类名或接口名(){……}

(3)【少用】因为匿名类不能定义构造方法,所以如果new的对象带参数,则只能使用父类的有参构造方法。

3.匿名类的应用

(1)用到界面的事件处理

(2)作为方法的参数,常见是继承某个类、实现某个接口

3.8.1包装类、枚举、注解—浅识包装类

1.基本类型的包装类:由于基本类型不能当作对象使用,所以就有了将基本类型(primitive type)包装成引用类型的包装类(Wrapper),八种:Boolean, Byte, Short, Character, Integer, Long, Float, Double

2.用法

(1)以前:Integer I = new Interger(10);

(2)新用法:

①装箱:Interger I = 10;  //把一个基本类型直接赋值给包装类型

②拆箱:int i = I;  //把一个包装类型直接赋给基本类型

注:编译器会分别译为:

Interger I =Interger.valueOf(10);

                     int I = I.intValue();

3.包装类主要用于对象数组中

如:Object[ ] ary={1,“aaa”};//把int 的整数1,直接赋给对象数组的元素时会发生自动装箱
4.包装类的优点

3.8.2包装类、枚举、注解—浅识枚举

1.枚举:Java中枚举(enum)是一种特殊的class类型,与其它语言底层是整数的枚举类型完全不同。

2.使用方法:

(1)简单情况下,使用方法与其他语言的枚举(enum)相似:

  1. enum Light { Red, Yellow, Green };//定义一个枚举,取一个名字,它有几个常量  
  2. Light light= Light.Red; //用的时候,定义一个枚举型的变量  
  3. //但实际上,它生成了class Light extends java.lang.Enum  

(2)自定义枚举:可以在enum定义体中,添加属性变量、方法、构造方法

  1. enumDirection  
  2. {  
  3.       EAST("",1), SOUTH("",2),WEST("西",3), NORTH("",4);  
  4.       private Direction(String desc, intnum){  
  5.             this.desc=desc; this.num=num;  
  6.       }  
  7.       private String desc;  
  8.       private intnum;  
  9.       public String getDesc(){   
  10.             return desc;  
  11.       }  
  12.       public intgetNum(){   
  13.             return num;   
  14.       }  
  15. }  

3.8.3包装类、枚举、注解—浅识注解

1.注解(annotation),又称为注记、标记、标注、注释(不同于comments)。是在各种语法要素(类、属性、方法)上加上附加信息,以供编译器或其他程序使用。

2.所有的注解都是java.lang.annotation. Annotation的子类。常用注解,如

    @Override 表示覆盖父类的方法

    @Deprecated 表示过时的方法,即编译器会提醒更换新方法

@SuppressWarnings表示让编译器不产生警告

3.9没有指针的Java语言

1.Java的引用(reference)的实质是指针(pointer),但这种指针是受控的、安全的。比如:

(1)会检查是否有空指引

(2)没有指针运算,避免内存的错误访问

(3)不能访问没有引用到的内存,使得自动垃圾回收成为可能

2.==

简单地说,基本类型是判断值是否相等,引用类型是判断引用是否相等(是否引用到同一个对象实体上)但有不少的具体情况需要具体分析。

3.基本数值型的==判断

    (1)若数值的类型不同,则先转换再比较

    (2)浮点数,不能直接用==判断是否相等(因为浮点数有误差)

4.引用类型相等的判断

    (1)==是判断两个引用型变量的引用是否相同,并并非判断内容是否相同

    (2)如果要判断两个引用的实体的内容是否一样,则需重写equals方法

①字符串类,有已经重写好的equal方法直接使用

②如果是判断自己写的类的引用变量的内容是否一样,则需重写equals方法,且最好重写hashCode()方法(给对象分别计算出一个值,得到结果后快速判断两个对象是否相等)

5.思考:

  1. Integer i= new Integer(10);  
  2. Integer j = new Integer(10);  
  3. System.out.println(i==j); //false,两个引用分别指向两个不同的对象,即引用不等  
  4.   
  5. Integer m = 10;  
  6. Integer n = 10;  
  7. System.out.println(m==n); //true,因为自动装箱过程调用的是intValue( ),这个方法对于-128~127的数有缓存  
  8.   
  9. Integer p = 200;  
  10. Integer q = 200;  
  11. System.out.println(p==q); //false,自动装箱过程调用的是intValue( ),这个方法对于不在-128~127的数无缓存 

注意:装箱对象的是否相等要注意缓存:If the value p being boxed is true, false, a byte, or a char in the range \u0000 to \u007f, or an intor short number between -128 and 127 (inclusive), then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2.

6.引用类型String对象的特殊性:判断引用是否相等,用==;判断内容是否相等,用equals。但字符串常量(String literal)会进行内部化(interned)。简单说:相同的字符串常量也是==的。

例TestStringEquals.java

  1. String hello = "Hello", lo = "lo";//两个字符串型引用变量分别引用字符串常量  
  2. System.out.println( hello == "Hello"); //true,相同的字符串常量是==  
  3. System.out.println( Other.hello== hello ); //true,凡是相同的字符串常量是==  
  4. System.out.println( hello == ("Hel"+"lo") ); //true,编译器会合并,相同的字符串常量是==  
  5. System.out.println( hello == ("Hel"+lo) ); //false,常量+引用  
  6. System.out.println( hello == new String("Hello")); //false,常量与引用  
  7. System.out.println( hello == ("Hel"+lo).intern ()); //true常量+引用的内部化与字符串常量  

注:文本由H同志编写,欢迎批评指正、交流探讨;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

.yh21

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值