目录
方法覆盖Override
-
方法覆盖初体验,什么样的程序要方法覆盖
/* 当前程序存在的问题(设计上的问题)?? 鸟儿在执行move()方法的时候,最好输出的结果是:“鸟儿在飞翔” 但是当前的程序在执行move()方法时输出的结果是:动物在移动 */ public class OverrideTest01 { public static void main(String[] args){ //创建鸟儿对象 Bird b = new Bird(); //让鸟儿动 b.move(); //创建一个猫 Cat1 c = new Cat1(); //让猫动 c.move(); } } //父类 class Animal1{ //可能这个方法不需要改动 public void doSome(){ } //而这个方法需要改动。 //移动 public void move(){ System.out.println("动物在移动!"); } } //子类 class Bird extends Animal1{ //子类继承父类后,有一些“行为”可能不需要改进,而有一些“行为”必须改进 //因为父类中继承过来的方法已经无法满足子类的业务需求。 //鸟儿在移动的时候要输出:鸟儿在飞翔!! } class Cat1 extends Animal1{ //猫在移动时希望输出:喵喵在走猫步!! }
-
什么时候构成方法覆盖以及注意事项
/* 回顾一下方法重载: 什么时候考虑使用方法重载Overload? 在一个类中,几个方法功能相似时,建议将方法名同名,这样代码美观,又方便编程 满足什么条件时构成方法重载? 1、同一个类中 2、方法名相同 3、参数列表不同 (个数/顺序/类型 不同,都算不同) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 什么时候我们会考虑使用“方法覆盖”? 子类继承父类之后,继承过来的方法无法满足当前子类的业务需求时,子类有权对这个方法进行重写 有必要进行“方法的覆盖” 方法覆盖又叫:方法重写(重新编写)、英:Override、Overwrite 比较常见的:方法覆盖、方法重写、override。 重要结论: 当子类对父类继承过来的方法进行“方法覆盖”之后,子类调用该方法时,执行覆盖之后的方法 什么情况构成方法覆盖 1、两个类必须要有继承关系 2、两个方法必须有: 相同的返回值类型 相同的方法名 相同的形式参数列表 3、访问权限不能更低,可以更高。 4、重写之后的方法不能比之前的方法抛出更多的异常,可以更少。 注意事项: 1、方法覆盖只是针对方法,与属性无关。 2、私有方法无法覆盖 3、构造方法不能继承,所以构造方法也不能被覆盖 4、方法覆盖只是针对“实例方法”,“静态方法”的覆盖没有意义 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - 方法重载和方法覆盖有什么区别? 1、方法重载发生在同一个类中 2、方法覆盖是发生在具有继承关系的父类与子类之间 3、方法重载是在同一个类中,方法名相同,参数列表不同。 4、方法覆盖是具有继承关系的父子类,并且重写之后的方法必须和之前的方法一致: 方法名、参数列表 以及 返回值类型的一致。 */ public class OverrideTest02 { public static void main(String[] args){ Animal2 a = new Animal2(); a.move(); Bird2 b = new Bird2(); b.move(); Cat2 c = new Cat2(); c.move(); //这两个方法不构成覆盖,构成重载 c.sing(); c.sing(100); } } class Animal2{ // public void move(){ // System.out.println("动物在移动"); // } //如果原来是protected,则不会报错 protected void move(){ System.out.println("动物在移动"); } public void sing(int sing){ System.out.println("动物在嚎叫"); } } class Bird2 extends Animal2{ //对move方法进行方法覆盖 //最好的方法是将原方法直接复制过来,然后修改。 //是将原来的方法覆盖掉了,继承过来的方法跟被注释掉一样。 public void move(){ System.out.println("bird在飞翔!"); } //protected 表示受保护的,没有public开放 /* 3 错误: Bird2中的sing(int)无法覆盖Animal2中的sing(int) 正在尝试分配更低的访问权限; 以前为public protected void sing(int s){ System.out.println("喵"); } */ } class Cat2 extends Animal2{ public void move(){ System.out.println("喵喵走猫步!"); } //此方法与父类中sing(int sing)有没有构成方法覆盖? //没有,因为这是两个不同的方法,因为他们的参数列表不同 //但是此方法构成方法重载,原因:子类继承父类,父类中的所有都相当于复制了一份到子类中,可以算是在同一个类中 //这两个方法名字相同,参数列表不同,所以构成重载 public void sing(){ System.out.println("喵喵喵"); } /* 4、 错误: Cat2中的move()无法覆盖Animal2中的move() 被覆盖的方法未抛出java.lang.Exception 错误:未报告的异常错误java.lang.Exception; 必须对其进行捕获或声明以便抛出 public void move()throws Exception{ System.out.println("喵喵走猫步!"); } */ }
-
方法覆盖实例
/* 方法覆盖案例 注意:方法覆盖/重写时,建议将父类的方法复制过来再修改 原因:可能方法名的某个字母大写/写错,就不会发生方法覆盖了,还是调用原来的方法 不好找到错误,可能你会以为是其他问题,找了半天发现名字错了,解决这种问题浪费时间 */ public class OverrideTest03 { public static void main(String[] args){ //这里错误:没有在子类中提供有参数的构造方法 //Chinese cp = new Chinese("zhong"); //这里可以,因为默认提供无参的构造方法 Chinese2 cp = new Chinese2(); cp.setName("张三"); cp.speak(); American ap = new American(); ap.setName("Jack"); ap.speak(); } } class People{ private String name; public People(){} public People(String name){ this.name = name; } public String getName(){ return name; } public void setName(String name){ this.name = name; } //人都会讲话 public void speak(){ System.out.println(name +"...."); } } class Chinese2 extends People{ //中国人说中国话,进行方法重写 public void speak(){ //这里是子类访问父类中的私有例变量name ,要用方法调用。 //System.out.println(name +"bb...."); //this.表示当前对象,可以省。不省比较好。 System.out.println(this.getName() +"逼逼叨叨...."); } } class American extends People{ //美国人说英语,进行方法重写 public void speak(){ System.out.println(this.getName() +"bb...."); } }
-
toString()的方法覆盖
/* 大多数的toSting方法都是需要覆盖的。因为Object中提供的toString()方法输出的是一个对象的内存地址 进行覆盖的格式可以自定义,或者看项目需求。 关于Object中的toString()方法 1、toString()方法的作用是什么? 作用:将“java”对象转换成“字符串”的形式 2、Object类中toString()方法的默认实现是什么? public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } toString 方法名的意思:转换成String 含义:调用一个java对象的toString()方法就可以将该java对象转换成字符串的表示形式 3、 那么toString()方法的默认实现够用吗? */ public class OverrideTest04 { public static void main(String[] args){ //创建日期对象 MyDate md = new MyDate(); //调用toString()方法(将对象转换成字符串形式) //System.out.println(md.toString()); //MyDate@1b6d3586 //问:对这个输出结果满不满意? 答:不满意 理想输出:xxxx年xx月xx日 //当父类中的Object方法中提供的toString()方法不满足需求时,需要进行方法覆盖。 //重写后 System.out.println(md.toString()); //1999年9月9日 //当输出一个“引用”时,println会自动调用该引用的toString()方法 System.out.println(md); //1999年9月9日 MyDate md2 = new MyDate(2000,11,11); System.out.println(md2); //2000年11月11日 //创建一个学生对象 Student7 s = new Student7(110,"张三"); //重写之前 //System.out.println(s.toString()); //Student7@1b6d3586 //输出一个对象之后,可能更愿意看到学生的信息,而不是这个学生对象的内存地址,此时对它进行重写 //重写之后 System.out.println(s.toString()); //学号为110的学生名字:张三 } } class MyDate{ private int year; private int month; private int day; public MyDate(){ this(1999,9,9); } public MyDate(int year,int month,int day){ this.year = year; this.month = month; this.day = day; } public int getYear(){ return year; } public void setYear(int year){ this.year = year; } public int getMonth(){ return month; } public void setMonth(int month){ this.month = month; } public int getDay(){ return day; } public void setDay(int day){ this.day = day; } //在子类中对不满足需求的toString()这个父类继承过来的方法进行覆盖 //要求:调用toString方法的时候输出:xxxx年xx月xx日 //重写时一定要复制粘贴,不能手动写,可能会错。 public String toString(){ return year + "年" + month + "月" + day + "日"; } } class Student7{ int no; String name; public Student7(){ } public Student7(int no,String name){ this.no = no; this.name = name; } //重写 public String toString(){ return "学号为" + no + "的学生名字:" + name ; } }
多态
-
多态的基础语法
向上/向下转型 ,多态是什么,什么时候使用向下转型
//父类:动物类 public class Animal3 { public void move(){ System.out.println("动物在移动"); } }
//子类:猫类 public class Cat3 extends Animal3{ //重写 public void move(){ System.out.println("猫在走猫步"); } //猫除了move外,还有自己特有的行为,比如:抓老鼠 public void catchMouse(){ System.out.println("猫正在抓老鼠"); } }
//子类:鸟儿类 public class Bird3 extends Animal3{ public void move(){ System.out.println("鸟儿在空中闪现"); } public void sing(){ System.out.println("鸟儿在唱歌!"); } }
//不继承Animal3 public class Dog3 { public void move(){ System.out.println("狗在疾跑"); } }
/* 多态的基础语法: 1、学习多态基础语法之前,我们需要普及两个概念: 第一: 向上转型(upcasting) 子 ---- > 父 (类似于:自动类型转换) Animal a = new Cat(); 第二: 向下转型(downcasting) 父 ---- > 子 (类似于:强制类型转换,需要加强制类型转换符) 注意: ★重点★:无论是向上/向下转型,两个类之间必须有继承关系,没有继承关系编译器会报错。 1、java中允许 向上转型,也允许 向下转型 2、引用类型转换包括 :向上转型、向下转型。与基本数据类型的自动/强制类型转换 原理相似。 但引用类型没有 自动类型转化/强制类型转换这种说法 因为 自动/强制类型转换 是用于基本数据类型的 2、多态是指: 父类型“引用”指向 子类型“对象” 包括:编译阶段、运行阶段 编译阶段:静态绑定父类的方法 运行阶段:动态绑定子类对象的方法。 3、什么时候必须使用“向下转型”? 不能随便向下转型。 当你需要访问的是子类对象中“特有”的方法时,必须进行向下转型。 */ public class Test01 { public static void main(String[] args){ Animal3 a1 = new Animal3(); a1.move(); //动物在移动 Bird3 b1 = new Bird3(); b1.move(); //鸟儿在空中闪现 Cat3 c1 = new Cat3(); c1.move(); //猫在走猫步 /*可以这样写吗? 1、Animal3 和 Cat3 之间有继承关系吗? 有的 2、Animal3 是父类 , Cat3是子类。 3、Cat is a Animal 这句话可以说通。 4、经过测试得知java中支持这样的语法: 父类的引用 允许指向 子类的对象。 Animal3 a2 = new Bird3(); a2就是父类型的引用 new Bird3() 是创建了一个子类型的对象 允许a2这个父类的引用 指向 Bird3()的对象 */ Animal3 a2 = new Bird3(); Animal3 a3 = new Cat3(); //没有继承关系的两个类型之间不存在转型 //错误:不兼容的类型: Dog3无法转换为Animal3 //Animal3 ad = new Dog3(); //调用a2与a3的move()方法。 /* 什么是多态? 多种形态,多种状态 分析:a2.move(); java程序分为编译阶段和运行阶段 - 编译阶段(属于静态绑定): 对于编译器来说,编译器只知道a2的类型是Animal3。 所以检查语法时会去Animal3中找move()方法,找到后绑定move()方法,编译通过后,静态绑定成功 - 运行阶段(属于动态绑定): 运行阶段,实际上是堆内存中创建的java对象是Cat3对象,所以move()的时候,真正参与move的对象是一个cat,所以运行阶段会动态执行Cat3对象的move方法。 多态表示多种形态:编译时一种形态、运行时一种形态。 */ a2.move(); //鸟儿在空中闪现 a3.move(); //猫在走猫步 //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Animal3 a5 = new Cat3(); //底层对象是一只猫 //a5.catchMouse(); /* 分析能否编译和运行? 分析程序一定要分析编译阶段的静态绑定和运行阶段的动态绑定 只有编译动过的代码才能运行,编译不通过,根本没有运行的机会 错误: java: 找不到符号 符号: 方法 catchMouse() 位置: 类型为Animal3的变量 a5 原因: 编译器只知道a5的类型是Animal3,去Animal3.class文件中查找catchMouse()方法时,发现没有该方法,所以静态绑定失败,编译报错,无法运行(语法不通过) */ //假设进行到这里,必须要调用catchMouse()方法,应该怎么操作? //这时就必须使用“向下转型”了。 //以下代码编译通过 //a5是Animal3类型,转成Cat,Animal3和Cat3之间存在继承关系,所以编译通过。 Cat3 x = (Cat3)a5; x.catchMouse(); //猫正在抓老鼠 //向下转型有风险吗? 有 Animal3 a6 = new Bird3(); //表面上(编译时)a6是一个Animal3对象,运行时实际上是一个鸟儿 /* 分析以下程序:编译报错还是运行报错? Cat3 y = (Cat3)a6; y.catchMouse(); 编译阶段:编译器检测到a6这个引用是Animal3类型,而Animal3与Cat3之间存在继承关系 可以向下转型,编译通过。 运行阶段:堆内存实际创建的对象是:Bird3对象,实际运行时,Bird3对象转换成Cat3对象 就会报错,因为Bird3与Cat3之间没有继承关系。 运行时出现错误: java.lang.ClassCastException 类型转换异常。 ★这个异常非常经典、非常重要. */ /* 怎么避免ClassCastException异常的发生? 新的内容: instanceof (运行阶段动态判断) 1、instanceof 可以在运行阶段动态判断引用指向的对象的类型。 2、instanceof 语法: (引用 instanceof 类型) 3、instanceof运算符的运算结果只能是:true/false 4、c是一个引用,c变量保存了内存地址指向了堆中的对象。 假设(c instanceof Cat) 为true c引用指向的堆内存中的java对象是一个Cat 假设(c instanceof Cat) 为false c引用指向的堆内存中的java对象不是一个Cat。 */ System.out.println(a6 instanceof Cat3); //false if(a6 instanceof Cat3){//如果a6是Cat3类型的,才会执行向下转型 Cat3 y = (Cat3)a6; //a6是Cat3的对象,才会执行。如果不是则不执行。 y.catchMouse(); } } }
向下转型必须使用instanceof进行判断的原因。
public class AnimalTest { //Test方法是程序员B编写 //这个Test()方法的参数是一个animal public void Test(Animal3 a){ //这个方法别人会去调用 //别人调用的时候可能会穿过来bird/cat,我们不知道他调用时会传一个什么进来。 //所以用instanceof判断之后就能很好的调用子类特有的方法。防止类型转换异常的出现。 if(a instanceof Cat3){ Cat3 c = (Cat3)a; c.catchMouse(); }else if(a instanceof Bird3){ Bird3 b =(Bird3)a; b.sing(); } } }
/* 疑问: 可以看到底层是new Bird3()/new Cat3()!,为什么还要使用instanceof进行判断? 原因: 以后可能看不到。 */ public class Test02 { public static void main(String[] args){ Animal3 ab = new Bird3(); Animal3 ac = new Cat3(); if(ab instanceof Bird3){ Bird3 b = (Bird3)ab; b.sing(); }else if(ab instanceof Cat3){ Cat3 c =(Cat3)ab; c.catchMouse(); } if(ac instanceof Bird3){ Bird3 b = (Bird3)ac; b.sing(); }else if(ac instanceof Cat3){ Cat3 c =(Cat3)ac; c.catchMouse(); } //这里程序员A负责编写 AnimalTest at = new AnimalTest(); at.Test(new Cat3()); at.Test(new Bird3()); } }
-
※★多态在开发中的作用★※
多态在开发中的作用是:降低耦合度,提高程序的扩展力。
public class Master{ public void feed(Dog d){ d.eat(); } public void feed(Cat c){ c.eat(); } }
以上的代码中表示:Master与Dog和Cat之间的关系很紧密(耦合度高),从而导致了扩展力差
public class Master{ public void feed(Pet pet){ pet.eat(); } }
以上表示:Master与Dog以及Cat的关系脱离了(耦合度降低),Master关注的是Pet类,提高了软件的扩展性
/* 2.1.多态练习题 编写程序模拟“主人”喂养“宠物”的场景: 提示1: 主人类:Master 宠物类:Pet 宠物类子类:Dog、Cat、YingWu 提示2: 主人应该有喂养的方法:feed() 宠物应该有吃的方法:eat() 只要主人喂宠物,宠物就吃。 要求:主人类中只提供一个喂养方法feed(),要求达到可以喂养各种类型的宠物。 编写测试程序: 创建主人对象 创建各种宠物对象 调用主人的喂养方法feed(),喂养不同的宠物,观察执行结果。 通过该案例,理解多态在开发中的作用。 重要提示:feed方法是否需要一个参数,参数选什么类型!!! */ public class Pet { public void eat(){} }
public class Cat extends Pet{ public void eat(){ System.out.println("猫喜欢吃鱼,抓一条给他"); } }
public class Dog extends Pet{ public void eat(){ System.out.println("狗狗很喜欢吃肉,给他一块肉,他吃的很香"); } }
public class YingWu extends Pet{ public void eat(){ System.out.println("鹦鹉把小虫子都吃掉"); } }
//主人类 public class Master { /* //起初的时候主任只是喜欢养宠物狗 //喂养宠物狗狗 public void feed(Dog d){ d.eat(); } //新需求产生,导致我们“不得不”去修改Master这个类 public void feed(Cat c){ c.eat(); } */ //怎么做可以使Master类以后不再修改,即使以后又想养其他宠物,Master也不需要再修改。 //这个时候就需要使用:多态机制。最好不要写具体的宠物类型,这样会影响程序的拓展性 //public void feed(Cat c){c.eat(); } 如其中传递的参数是cat类型的,就会影响程序的拓展性。 public void feed(Pet pet){ //编译时,编译器会去pet类中找eat方法,找到后绑定 //运行时,底层实际的对象是什么,就自动调用该实际对象对应的eat()方法上 //这就是多态的使用 pet.eat(); } } /* 分析: 主人起初只想养狗,过了一段时间喜欢上养“猫” 在实际开发中这个就代表客户产生了新的需求。我们作为开发人员,必须满足客户的需求。 如何满足需求: 在不使用多态机制的前提下,目前我们只能在Master中添加一个新的方法 思考:软件在扩展需求过程中,修改Master这个类有什么问题? 软件在扩展过程中,修改的越少越好。修改的越多,系统当前的稳定性就越差,未知的风险更多 在这里涉及一个软件的开发原则: 软件开发中有七大原则(属于整个软件行业,不止属于java) 其中有一条最基本的原则:OCP(开闭原则) 什么是开闭原则? 拓展开放(可以额外添加),修改关闭(最好不要修改现有程序) 在软件拓展过程中,修改的越少越好 程序员开发项目不仅要实现客户的需求,还要考虑软件的拓展性 什么是软件拓展性 假设电脑上内存部件坏掉了,买一个新的插上就可以直接使用。这个电脑的设计就考虑了“拓展性”。 */
public class Test { public static void main(String[] args){ //创建主人对象 Master zhangSan = new Master(); //创建宠物狗对象 Dog d = new Dog(); //喂养宠物狗狗 zhangSan.feed(d); //创建猫对象 Cat c = new Cat(); //喂养猫咪 zhangSan.feed(c); //喂养鹦鹉 zhangSan.feed(new YingWu()); } }
-
面向对象的三大特征:
封装、继承、多态。一环扣一环
有了封装,就有了这种整体的概念。之后,对象和对象之间有了继承。继承之后,才有了方法的覆盖和多态
-
软件开发原则
七大原则中最基本的原则:OCP(对扩展开放,对修改关闭)
目的:降低耦合度,提高编程能力。面向抽象编程,不建议面向具体编程。
-
作业
/* 1.编写程序实现乐手弹奏乐器。乐手可以弹奏不同的乐器从而发出不同的声音。 可以弹奏的乐器包括二胡、钢琴和琵琶。 实现思路及关键代码: 1)定义乐器类Instrument,包括方法makeSound() 2)定义乐器类的子类:二胡Erhu、钢琴Piano和小提琴Violin 3)定义乐手类Musician,可以弹奏各种乐器play(Instrument i) 4)定义测试类,给乐手不同的乐器让他弹奏 */ public class Homework1 { public static void main(String[] args){ //创建个乐手 Musician mc = new Musician(); //让他弹二胡 mc.play(new ErHu()); //让他弹钢琴 mc.play(new Piano()); //让他拉小提琴 mc.play(new Violin()); } }
public class Instrument { public void makeSound(){ } }
public class Musician { public void play(Instrument i){ i.makeSound(); } }
public class ErHu extends Instrument { public void makeSound(){ System.out.println("二胡音响了"); } }
public class Piano extends Instrument{ public void makeSound(){ System.out.println("钢琴声响了起来"); } }
public class Violin extends Instrument{ public void makeSound(){ System.out.println("小提琴拉了起来"); } }
-
解决之前剩下的问题 私有不能覆盖;静态不谈覆盖
私有方法为什么无法覆盖
/* 私有方法不能覆盖 - java中不能覆盖private修饰的方法,因为private修饰的变量和方法都只能在当前类中使用。 如果是其他的类继承当前类,是不能访问private修饰的变量/方法的,当然也不能覆盖 - 覆盖是发生在字类和父类之间的,而private修饰的变量/方法无法被子类继承,子类无法访问,所以更没办法覆盖。 */ public class OverrideTest06 { public static void main(String[] args){ //多态 OverrideTest06 ot = new T0(); ot.doSome();//父类的私有方法doSome执行 //也就是说我们重写的公有方法没有执行,就说明私有方法不能覆盖。 } private void doSome(){ System.out.println("父类的私有方法doSome执行"); } } /* 在外部类中无法访问私有的。 class MyMain{ public static void main(String[] args){ OverrideTest06 ot = new T0(); //错误: doSome() 在 OverrideTest06 中是 private 访问控制 //ot.doSome(); } } */ class T0 extends OverrideTest06{ //尝试重写父类中的doSome 方法,访问权限不能更低,可以更高 public void doSome(){ System.out.println("子类的公有方法doSome执行"); } }
方法覆盖为什么只针对“实例方法”,对“静态方法“没有意义
/* 1、方法覆盖需要与多态机制联合起来使用才有意义。 Animal a = new Cat(); a.move(); 要什么效果? 编译时move()方法是Animal上的 运行时自动调用到子类写的move()方法上。 没有多态机制,方法覆盖也可有可无。 2、静态方法存在方法覆盖吗? 方法覆盖和多态密不可分,多态自然与对象有关系。而静态方法执行不需要对象 静态方法使用“类名.”调用,这种不能叫做覆盖。所以我们说静态方法“不存在覆盖” */ /* - static关键字表示:一个成员变量/方法可以在没有所属类的实例变量情况下访问。 - java中static方法不能覆盖,因为方法覆盖是基于动态绑定的,而static方法是编译时静态绑定的。 static方法跟类的任何实例都不相关,所以概念上不适用。 - static修饰的变量,表示该变量是类变量,属于类而不是属于实例对象,所以可以不通过实例化直接通过类进行访问,也不hi在实例对象内存空间释放后消失 static修饰的方法,表示该方法是类方法。在类加载后就可以通过类名调用,而不需要创建任何类的实例对象。 - 子类和父类的方法都必须是实例方法,如果父类是static方法而子类是实例方法(或相反),都会报错。 如果父类和子类都是static方法,那么子类隐藏父类的方法,而不是重写父类方法 - 方法覆盖/重写,是发生在子类和父类之间,有继承关系的类之间的,而且是基于动态绑定的,但static修饰的是静态绑定编译的。 */ public class OverrideTest05 { public static void main(String[] args){ Animal0 a = new Animal0(); Cat0 c = new Cat0(); //静态方法可以使用“引用.”来调用吗? a.doSome(); //Animal静态 c.doSome(); //Cat静态 //虽然使用“引用.”来调用,但是和对象无关。实际运行时还是: Animal0.doSome(); Cat0.doSome(); } } class Animal0{ //父类的静态方法 public static void doSome(){ System.out.println("Animal静态"); } } class Cat0 extends Animal0{ //子类的静态方法 public static void doSome(){ System.out.println("Cat静态"); } }
学习了多态之后,方法覆盖的条件:”返回值类型相同“可以修改一下吗?
返回值如果是基本数据类型,想要覆盖,返回值类型必须一致
返回值如果是引用数据类型,返回值类型可以变得更小(但意义不大,实际开发中没人这样写)