继承与多态

类的继承

一、继承

  1. 类之间的关系
    • HAS-A关系(子对象或对象成员):
      • 类之间的包含关系、聚合和分解关系,即类A的对象作为类B的成员变量
    • IS-A关系(继承或派生):
      • 继承关系,概括和特化的关系,即类B继承类A的属性和行为
  2. 继承的含义
    1. 继承机制提供了一种充分利用现有资源的方法
    2. 它既体现了经济思想,又体现了谦虚精神
    3. 派生类是父类的特殊化,是对父类在功能、内涵方面的扩展和延伸;在多继承时,派生类是父类的组合
  3. 继承的优点
    • 继承使得在创建新类时,只需说明新类与父类的区别, 从而大量原有代码都可复用
    • 提高了程序代码的重用性和程序的可扩充性
    • 缩短了软件开发的周期
    • 使得程序结构清晰
    • 降低了编码和维护的工作量,提高了程序开发效率
    • 提高了程序的抽象程度
    • 更接近人类的习惯思维方式
  4. 继承机制
    • 继承:子类继承父类的状态和行为
    • 覆盖隐藏改造修改:子类也可以修改父类的状态或重载父类的行为
    • 扩充:子类可以添加新的状态和行为
  5. 继承的概念和特点

    • 单继承:子类只能有一个直接父类
    • 多继承:子类可以有多个直接父类
    • 类的层次结构:一个父类可以是另一个父类的派生类(子类)
    • 继承具有传递性
    • 从成员数目角度看,通常子类成员数目比父类多,子类比父类大
    • Java只允许单继承,通过接口可以间接实现多继承

      二、类的继承

  6. Java中,所有的类都是通过直接或间接地继承java.lang.Object得到的

  7. 类继承并不改变成员的访问权限,父类中的公有和保护成员被继承到子类后,在子类中仍然作公有和保护成员。
  8. 父类中的私有成员在子类中不可见(不可使用)
  9. 子类将继承其直接父类中的所有非私有成员变量和成员方法
  10. 派生类有两种成员:
    1. 从父类继承来的成员(父类子对象),须由父类构造方法初始化
    2. 派生类自定义成员,由派生类自己的构造方法初始化。
  11. 创建子类对象时,系统先调用父类构造方法将从父类继承来的成员初始化,再调用子类构造方法初始化子类自己新定义的成员

    三、类继承的实现

  12. 格式: [修饰符] class 类名 extends 父类名 {类体;}

  13. 说明:
    1. 类体:即子类自定义新成员
    2. 若一个类没有显式继承其他类,则默认继承Object类
    3. 须先定义父类,再定义子类
    4. 子类对象和成员方法不能直接访问父类的私有成员
      1. 例://D.java
      2. class A {
      3. int x=2;
      4. void f(){System.out.println(x);}
      5. }
      6. class B extends A{}
      7. public class D {
      8. public static void main(String []args) {
      9. A a=new A();//a.x=2
      10. a.f();//2
      11. B b=new B();//b.x=2
      12. b.x=3;
      13. b.f();//3
      14. }
      15. }
      ```java 例://D.java class A{ int x=2; void f(){System.out.println(x);} } class B extends A { int y=3; void g(){System.out.println(“x=”+x+”y=”+y);} } public class D { public static void main(String []args) { B b=new B();//b.x=2 b.y=3 b.f();//2 b.g();//x=2y=3 A a=new A();//a.x=2 a.f();//2 //a.g();非法 } }

例://D.java class A { int x=2; A(){System.out.println(“x=”+x);} } class B extends A { int y=3; B(){System.out.println(“y=”+y);} } class D { public static void main(String []args) { A a=new A();//x=2 B b=new B();//x=2y=3 B m=new B();//x=2y=3 } }

  1. <a name="btGCR"></a>
  2. ####
  3. <a name="Bh8Fw"></a>
  4. ### this和super
  5. <a name="TQuJZ"></a>
  6. #### 一、改造父类成员
  7. 1. 子类在继承父类成员的基础上,可以通过自定义新 成员来扩充功能
  8. 2. 控制父类成员在子类中的访问权限
  9. 1. 类继承并不改变成员的访问权限,所以调节父类中成员的访问权限,即可控制父类成员在子类中的访问 权限
  10. 2. 若父类不允许子类访问它的某些成员,则须将其声明为private成员
  11. 3. 对父类成员进行同名覆盖
  12. 1. 父类成员不一定能完全适合子类的需要
  13. 2. 继承有用成员的同时,子类也继承了它不需要的父类成员
  14. 3. 当一个父类成员不适合该子类时,子类可以覆 盖和重新定义它
  15. 4. 同名覆盖可扩充和改造父类成员
  16. 4. 同名覆盖
  17. 1. 即在派生类中声明一个与父类成员同名的新成员以屏 蔽父类同名成员
  18. 2. 在子类中或通过子类对象只能访问到子类中新定义的 同名成员
  19. 3. 同名覆盖隐藏成员变量时,要求父类和子类同名成员 变量的类型和名字相同
  20. 4. 同名覆盖成员方法时,要求父类和子类同名成员方法 的方法头(函数原型)完全相同,否则为方法重载或 编译错误
  21. 5. 同名覆盖是一种多态
  22. 6. 同名覆盖后,父类同名成员还存在,只是看不见(被隐藏,被屏蔽)
  23. 7. 在子类中通过super可改变被覆盖的父类同名成员的可见性,即在子类中通过super可显式调用父类的同名覆盖成员
  24. ```java
  25. 例://D.java
  26. class A {
  27. void f(){System.out.println("AA");}
  28. void g(){System.out.println("aa");}
  29. }
  30. class B extends A {
  31. void f(){System.out.println("BB");}
  32. void g(int x){System.out.println("bb"+x);}
  33. }
  34. class D {
  35. public static void main(String []args) {
  36. B b=new B();
  37. b.f();//BB
  38. b.g();//aa
  39. b.g(2);//bb2
  40. }
  41. }
  42. 例://D.java
  43. class A {
  44. int x=2;
  45. void f(){System.out.println(x);}
  46. }
  47. class B extends A {
  48. int x=3;
  49. void g(){System.out.println(x);}
  50. void h(){f();}
  51. }
  52. class D {
  53. public static void main(String []args) {
  54. B b=new B();
  55. b.f();//2
  56. b.g();//3
  57. b.h();//2 调用方法的时候,对子类不可见
  58. }
  59. }

二、this

  • Java系统默认,每个类都具有null,this和super三个域, 在任何类中都可以不加说明而直接使用它们
  • null:空引用 /this:代表当前对象的引用 /super:代表父类对象的引用
  1. this:
    1. 表示当前对象本身,代表了当前对象的一个引用(别 名)
    2. 一个对象可以有若干个引用,this就是其中之一
    3. 利用this可以访问当前对象的成员
    4. 常用来把当前对象的引用作为参数传递给其他的对象 或方法
    5. 由系统自动创建,自动使用,用户也可以显式使用
    6. 不能在程序中显式修改它或给它赋值
    7. 静态方法(即类方法)中不能使用this
    8. 它始终代表当前对象(正在被某个成员方法操 作的对象)
  2. 使用this来访问当前对象的成员

    1. 在所有非static方法中,都隐含了一个参数this
    2. 提高了程序的可读性
    3. this作这种用法时,通常可以省去this(系统默认为this)
    4. 格式:this·成员变量 /this·成员方法(参数值);
      1. 例://B.java
      2. class A {
      3. int x=2;
      4. A(int y){x=y;} //等价于this.x=y;
      5. void f(A c){
      6. System.out.println("this.x="+this.x+"c.x="+c.x);
      7. }
      8. }
      9. class B {
      10. public static void main(String []args) {
      11. A a=new A(3); //a.x=3
      12. A b=new A(4); //b.x=4
      13. a.f(b); //this.x=3 c.x=4
      14. }
      15. }
  3. 使用this解决局部变量与成员变量同名的问题

    1. 在Java的成员方法中,当局部变量与成员变量同名时,成员变量被隐藏(看不见)
    2. 通过this可以改变成员变量的可见性
    3. 在成员变量前面加上this,以此区别于局部变量
      1. 例://B.java
      2. class A {
      3. int x=2;
      4. void f(int x){this.x=x;}
      5. void g(int x){x=this.x;}
      6. void s(){System.out.println(x);}
      7. }
      8. class B {
      9. public static void main(String []args) {
      10. A a=new A();//a.x=2
      11. a.f(3);//a.x=3
      12. a.s();//3
      13. a.g(4);//a.x=3
      14. a.s();//3
      15. }
      16. }
  4. 构造方法中,用this调用本类另一个构造方法

    1. 常用于重载的构造方法之间的相互调用
    2. 可提高代码的重用率,提高程序的抽象封装程度,减 少程序维护的工作量
    3. 此时,this调用语句必须是构造方法中的第一个可执行语句
      1. 例://B.java
      2. class A {
      3. int x=2;
      4. A(int y){x=y;}
      5. A(){this(3);}
      6. A(int m,int n){this(m+n);}
      7. void f(){System.out.println(x);}
      8. }
      9. class B {
      10. public static void main(String []args) {
      11. A a=new A(); //a.x=3
      12. a.f();//3
      13. A b=new A(4,5); //b.x=9
      14. b.f(); //9
      15. }
      16. }
  5. this的其他应用

    1. this作方法的返回值或方法的实在参数时,代表当前对象
      1. 例://A.java
      2. public class A {
      3. private int n=0;
      4. A f(){n++;return this;}
      5. void s(){System.out.println(n);}
      6. public static void main(String []args) {
      7. A a=new A(); //a.n=0
      8. a.f().f().f().s();//3
      9. }}

三、super

  1. super:
    • 表示当前对象的直接父类对象,是当前对象的直接父 类对象的引用
    • 由系统自动定义,可直接使用
    • 静态方法(即类方法)中不能使用super
    • 不能在程序中显式修改它或给它赋值
  2. 使用super访问子类对象中从父类 继承来的成员

    1. 使用super标记标明是当前对象访问它从父类中继承来 的成员
    2. 此时,通常super可以省略
    3. 它强调是当前对象(子类对象)的super
    4. 格式:super·父类成员变量名 /super·父类成员方法名(参数值)
      1. 例://D.java
      2. class A {int x=2;}
      3. class B extends A {
      4. void s(){x=3;this.x=4;super.x=5;}
      5. void f(){System.out.println(x+","+this.x+","+super.x);}
      6. }
      7. class D {
      8. public static void main(String []args) {
      9. B b=new B();//b.x=2
      10. b.f();//2,2,2
      11. b.s();//b.x=5
      12. b.f();//5,5,5
      13. }
      14. }
  3. 使用super访问被子类同名覆盖的 父类成员

  4. 使用super调用父类的构造方法

    1. 在子类的构造方法中,可以用super来调用父类的构造 方法
    2. 此时,super调用语句必须是子类的构造方法中的第一 条可执行语句,以保证“先兄长再自己”
    3. 即先调用执行父类构造方法初始化从父类继承来的成 员
    4. 再执行本类构造方法初始化自定义新成员
    5. 格式:super(参数值);
      1. 例://D.java
      2. class A{
      3. int x=0;
      4. A(int y){x=y;}
      5. }
      6. class B extends A {
      7. //B(){super();} 非法
      8. B(int m,int n){super(m+n);}
      9. B(){super(2);}
      10. void f(){System.out.println(x);}
      11. }
      12. class D {
      13. public static void main(String []args) {
      14. B b=new B(); //b.x=2
      15. b.f(); //2
      16. B c=new B(3,4); //b.x=7
      17. c.f(); //7
      18. }
      19. }

      类的多态

      一、方法重载和同名覆盖

  5. 同一个类的成员方法间重载:要求方法形式参数的个数、类型、顺序不完全相同

  6. 不同类之间的成员方法重载:系统根据调用类对象名进行区分
  7. 子类和父类的成员方法之间也可以重载
  8. 同名覆盖只发生在有继承关系的类之间

    1. 例://D.java
    2. class A {
    3. void f(){System.out.println("AA");}
    4. void f(int x){System.out.println("AA"+x);}
    5. }
    6. class B extends A{
    7. void f(){System.out.println("BB");}
    8. }
    9. class D {
    10. public static void main(String []args) {
    11. A a=new A();
    12. a.f(); //AA
    13. a.f(2);//AA2
    14. B b=new B();
    15. b.f(); //BB
    16. b.f(3); //AA3
    17. }
    18. }

    二、赋值相容原理

  9. 子类对象可以赋值给父类对象引用,反之则不行

  10. 即派生类对象可自动隐式地转换为一个父类对象
  11. 在需要父类对象的任何地方,都可以使用其子类对象替代之
  12. 赋值相容性不可逆
  13. 是多态性的基础(运行时的多态),提高了编程效率
  14. 例:中国人是父类,成都人是其子类,则成都人必定是中国人,但反之中国人不一定是成都人
  15. 父类对象引用引用子类实例对象时,该引用只能访问与父类成员同名的成员
    1. 若成员变量被同名覆盖,访问的是从父类继承来得成员变量
    2. 若成员方法被同名覆盖,访问的是子类自定义的成员方法
  16. 当将子类对象赋值给父类对象引用时,该引用调用成员方法的规则是:
    1. 若子类同名覆盖了父类方法,则运行时系统就调用子类自定义的成员方法
    2. 若子类未同名覆盖父类方法,则运行时系统就调用从父类继承来的成员方法
  17. 当将子类对象赋值给父类对象引用时,该引用访问的成员变量,不论是否被同名覆盖,都是访问从父类继承来的成员变量
    1. 例://E.java
    2. class A{
    3. int x=2;
    4. void f(){System.out.println("AA");}
    5. }
    6. class B extends A {
    7. void g(){System.out.println("bb");}
    8. void f(){System.out.println("BB");}
    9. }
    10. class D extends A{int x=4;}
    11. class E {
    12. public static void main(String []args) {
    13. A a=new B();
    14. a.f(); //BB
    15. //a.g(); 非法
    16. a=new D();
    17. System.out.println(a.x); //2
    18. }
    19. }

final和abstract

一、final

  1. final修饰变量
    1. 该变量只能读,不能写,即为常量
    2. 只能赋一次值
    3. 通常用static与final一起使用来定义一个常量
  2. final修饰对象

    1. 该对象引用将不能再指向其他对象,但它所指对象的成员仍然可以改变
      1. 例://B.java
      2. class A {int x=2;}
      3. class B {public static void main(String []args) {
      4. final A a=new A(); //a.x=2
      5. a.x=3;
      6. System.out.println(a.x); //3
      7. //a=new A(); 非法
      8. }
      9. }
  3. final修饰方法

    1. 该方法将不能被覆盖,即最终方法
    2. 但final方法可以被重载
      1. 例://D.java
      2. class A {
      3. final void f(){System.out.println("AA");}
      4. }
      5. class B extends A {
      6. //void f(){System.out.println("BB");} 非法
      7. void f(int x){System.out.println("BB"+x);}
      8. }
      9. class D {
      10. public static void main(String []args) {
      11. B b=new B();
      12. b.f();//AA
      13. b.f(2);//BB2
      14. }
      15. }
  4. final修饰类

    1. 该类不能被继承,不能派生子类,即为最终类
    2. 该类不能作父类
    3. final类中的成员方法都被系统默认为是final方法
      1. 例://D.java
      2. final class A {
      3. void f(){System.out.println("AA");}
      4. }
      5. //class B extends A{} 非法
      6. class D {
      7. public static void main(String []args) {
      8. A a=new A();
      9. a.f(); //AA
      10. }
      11. }

      二、abstract

  • abstract类必须被继承派生/abstract方法必须被覆盖(重写)
  1. abstract方法

    1. 被abstract修饰的方法叫抽象方法
    2. 抽象方法的目的是为其所有子类定义一个统一的接口界面
    3. 抽象方法只需要声明,不需要实现,即用”;”表示方法体
    4. 抽象方法的实现由其派生类提供
    5. 抽象方法在子类中必须被实现,否则子类继承的仍为抽象方法
    6. 构造方法、静态方法(即类方法)和私有方法不能定义为抽象方法
    7. abstract不能与private、static、final或native并列修饰同一个方法
    8. 定义格式:abstract 类型符 抽象方法名(参数值表);
      1. 例://D.java
      2. abstract class A {
      3. abstract void f();
      4. void g(){System.out.println("AA");}
      5. }
      6. class B extends A {
      7. void f(){System.out.println("BB");}
      8. }
      9. class D {
      10. public static void main(String []args) {
      11. A a=new B();
      12. a.f(); //BB
      13. a.g();//AA
      14. }
      15. }
  2. abstract类

    1. 凡是被abstract修饰的类称为抽象类
    2. 它是没有具体对象的概念类(如:鸟类,它相对麻雀、鸽子、燕子等即为抽象类)
    3. 它是其所有子类的共性的集合
    4. 可提高程序开发和维护的效率
    5. 因为抽象类是不完整的类,故抽象类不能实例化对象
    6. 抽象类只能用作父类来派生子类
    7. 虽然抽象类不能实例化对象,但可以声明其对象引用
    8. 抽象类中可以包含抽象方法,也可以不包含abstract方法
    9. 包含抽象方法的类一定是抽象类,即abstract方法必须位于abstract类中
    10. 一旦某个类中包含了abstract方法,则该类必须声明为abstract类
    11. 子类必须实现父类中的全部抽象方法,否则子类仍然是抽象类,仍不能实例化对象
      1. 例://E.java
      2. abstract class A{abstract void f();}
      3. abstract class B extends A {
      4. void f(int x){System.out.println("BB"+x);}
      5. }
      6. class D extends B {
      7. void f(){System.out.println("DD");}
      8. }
      9. class E {
      10. public static void main(String []args) {
      11. A a=new D();
      12. a.f();//DD
      13. //a.f(2); 非法
      14. B b=new D();
      15. b.f(2); //BB2
      16. }
      17. }
  3. 类定义的完整格式

    1. [public][abstractfinal] class 类名 [extends 父类名][implements 接口名表] {
    2. [publicprotectedprivate][static] [final][transient][volatile] 类型符 成员变量名;
    3. [publicprotectedprivate][static] [finalabstract][native][synchronized] 类型符 成员方法名([参数表])[throws 异常列表] {方法体;}
    4. }

包和接口

1. 名字空间

  • 是能用名字区别不同事物的范围:包是一个类名空间,同一个包中的类(或接口)不能重名,不同包中的类可以重名
  • java没有全局变量和全局方法
  • 全限定名字:包名·类名·成员名

2. 包

  • 是若干相关的类、接口、(子)包的集合(容器)
  • 可以理解为文件夹,一个包对应一个文件夹
  • 是一种封装机制
  • 可以避免大量的类名冲突,扩大名字空间
  • java类库一包的形式储存
  • 同一个包中类名不能重复,不同包可以
  • 包名有层次关系,各层之间以·分隔,左大右小

3. 包的创建

  • package语句必须作为Java源文件的第一条语句
  • 表示该源文件中定义的全部类都属于这个包
  • 一个源文件中最多只能有一条package语句
  • 若缺省该语句,系统会为每一个*.java源文件创建一个无名包
  • 无名包中的类(因为没有包名)不能被其他包中的类所引用和复用,无名包也不能有子包
  • 创建包就是在当前文件夹下创建一个子文件夹, 以便存放该包中包含的所有类的*.class文件
  • Java要求包声明的层次和实际保存类的字节码文件的目录结构相对应
  • package语句的作用域是包含该语句的整个源文件
  • 格式:package 包名; 例:package a; 例:package java·awt;

4. 包的引用

  • 若要使用包中的类或接口,则必须将该包引入 (装载)到本源文件中
    1. import语句的三种形式
      1. import 包名;
        1. 例:import java.lang;
        2. 说明:该语句后面的程序凡涉及该lang包中的类和方法都可用简略形式书写
        3. 例:java.lang.System类可略写为lang.System
      2. import 包名·类名;
        1. 例:import java·lang·String;
        2. 说明:该语句后面的程序中可直接使用该类名
        3. 例 : 可 直 接 使 用 类 名 String 代替 java·lang·String
        4. 该方式适用于在源文件中只使用另一个包中的某一个类
      3. import 包名·*;
        1. 例:import java·applet·*;
        2. 说明:“*”为通配符,表示该包中的所有类
        3. 其后的程序中可直接使用该包中的所有类名
        4. 该方式适用于在源文件中使用另一个包中的多个类
        5. 注意:“*”只能表示本层次包中的所有类,而不包括其子层次下的类
        6. 例:import java·awt·;import java·awt·event·
    2. 包的引用说明
      1. import语句的数量没有限制
      2. import语句在源文件中必须位于package语句之后,类定义之前
      3. 若在源文件中偶尔使用另一个包中的类,也可以不用import语句,而直接在要引入的类名前加包名
      • 例:class A extends java·applet·Applet{…} 等价于 import java·applet·Applet;class A extends Applet{…}
      1. 用import引入包,这种方式适用于在源文件中多次使用另一个包中的类
      2. Java 系统为所有源程序文件自动引入包 java·lang
      3. 若引入的几个包中含有相同类名的类,则须在类名前加上包名前缀,以示区别
      • 例:创建a·b·D类的对象 a·b·D d=new a·b·D();

5. JDK中主要的包

  1. java·net: 包含执行与网络相关的操作的类,如: URL,Socket,ServerSocket等
  2. java·util:包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数

    接口

    一、接口

  1. 单继承和多继承
    1. 单继承
      1. 子类只能有一个直接父类,类层次为树状结构
      2. 类间关系简单,类间耦合度小
      3. 描述复杂问题的能力有限,当类层次较深时,间接父类多,子类成员数量庞大,难于控制和管理
    2. 多继承
      1. 子类可以有多个直接父类与人类的自然习惯思维一致,描述问题的能力强,但复杂
      2. 为了简化程序结构和类间的关系,Java不直接支持多继承
      3. Java通过接口来实现多继承

  2. 接口
  • 是方法定义(没有实现)和常量的集合
  • 它定义了若干个抽象方法和常量,是一种引用数据类型
  • 是一种特殊的抽象类
  • 提高了程序的抽象程度和简洁性
  • 一个类可以同时实现多个接口
  • 接口也具有继承性,且支持多继承
  • 一个接口可以有多个直接父接口,它们之间用逗号分隔
  • 子接口将继承所有父接口中的属性和方法
  • 接口中的属性都是用final修饰的常量
  • 接口中的方法都是用abstract修饰的抽象方法
  • 接口中不能实现任何方法(注意区别:抽象类中可以有方法实现)
  1. 接口的作用
    1. 可以实现不相关类的相同行为,而不需考虑这些类之间的层次关系
    2. 可以指明多个类需要实现的方法
    3. 可以了解对象的交互界面,而不需了解对象所对应的类
    4. 可以实现多继承

二、接口的定义

格式:
[public] interface 接口名 [extends 父接口名列表] {//接口体
//常量域声明 [public][static][final] 类型符常量名=常量值;
//抽象方法声明 [public][abstract][native] 类型符 方法名(参数列表) [throws 异常列表];
}

说明:
源文件名保存为:接口名·java
系统默认接口中的属性为:public static final
系统默认接口中的方法为:public abstract

  1. 例:
  2. interface A {int m=0; void f(); }
  3. interface B {int n=1; void g(); }
  4. interface D extends A,B {int k; public void s(); }
  5. interface E extends A {}

三、接口的实现

  • 所谓一个类实现一个接口,是指该类给出这个接口的所有行为的具体实现过程
  • 一个类在实现某接口的抽象方法时,必须使用完全相同的方法头
  • 接口中的抽象方法均默认为public,故在实现时必须显式地使用public修饰符
  • 在实现接口时,系统默认的修饰符(如public)是不能省略的
  • 一个类实现接口后,将继承接口中的所有静态常量,为该类所用
  • 若一个类只实现了接口中部分方法,则该类只能声明为abstract类
  • 仅当一个类实现了一个接口中的所有方法时,才能实例化

格式:[修饰符] class 类名 [extends 父类名] [implements 接口A,接口B,…] {
类自定义新成员;
实现接口A(为A中的所有方法编写方法体)
实现接口B(为B中的所有方法编写方法体)
…… }

  1. 例://E.java
  2. interface A{void f();}
  3. interface B{void g();}
  4. abstract class M {abstract public void s();}
  5. class D extends M implements A,B {
  6. public void f(){System.out.println("AA");}
  7. public void g(){System.out.println("BB");}
  8. public void s(){System.out.println("MM");}
  9. }
  10. class E {
  11. public static void main(String []args) {
  12. D d=new D();
  13. d.f();//AA
  14. d.g();//BB
  15. d.s();//MM
  16. }
  17. }

java源程序文件

完整的Java源文件格式:
package packageName; //指定文件中类所属的包,0句或1句
import packageName·[className∣*]; //指定引入的类,0个或多个
public classDefinition //定义public类,0个或1个
interfaceDefinition and classDefinition //接口或类定义,0个或多个