标签: Java

  • 继承和多态 | Java语言关键

    继承和多态 | Java语言关键

    前一篇封装的文章内说到,面向对象程序三大特性:封装、继承和多态。那么接下来讲下面两个特性:继承、多态。

    继承

    继承这里的概念,就是字面意思,核心就是要搞清楚“父与子”

    为什么会用到继承?试想一下,我们现在要做一个教务管理系统,那么其中的用户角色有学生、老师、管理员,我们肯定是对这些角色分别建一个类,不过每个类中有诸如头像、姓名、年龄等这些每个都相同的成员变量,那么我们真的要这三个类内每个人都写一遍这些吗?其实不然,我们想到这些人都是用户,那么我们可以做一个用户的父类,让这些角色的类进行继承。所以,用继承的作用是通过共性抽取去提高代码的复用和方便后续维护。比如,如果我发现每个人都漏了一个成员,那么我可以加到这些子类关联的父类里;如果我要给学生的类加特有的学号和班级的信息,那么我直接在学生子类内加即可。

    不过继承还有个作用就是实现多态,将在下文讲到。

    extends 关键字

    用于表示两个类间的继承关系是 extends 关键字,上述说明的代码如下(适当简化)。

    // User.java文件
    public class User {
        public String name;
        public int age;
    
    }
    
    // Teacher.java文件
    public class Teacher extends User {
        public int teacherId;
        public String teachClass;
    }
    
    // Student.java文件
    public class Student extends User {
        public int studentId;
        public String class;
    }

    extends 关键字前面是子类(也叫派生类),后面是父类(也叫基类、超类)。同样的,成员方法也可以从父类继承到每个子类。

    但是注意的是,Java不支持多父对一子的继承,即用代码表示如 public class Teacher extends User, TestUser

    同时也不推荐出现超过三层的继承关系,不要有太复杂的继承关系。

    子类访问父类

    当子类继承了父类后,可以访问父类中的成员变量和成员方法等。如果是子类成员变量和父类成员变量不同名的情况下,是可以直接访问的。如果是出现同名的情况下,优先访问子类的成员变量。但是如果子类中访问了父类不存在成员变量,显然会编译报错。由此,子类对成员变量的访问可以认为是就近原则

    对于成员方法,也是如此。不过之前在类和对象中讲过,方法是能重载的(即方法名相同,参数列表和返回值不同的情况),那么如果父类和子类间的相同方法名的时候,可以按需要选择适合的方法访问。

    成员方法中还有构造方法,要注意的是,应该是先调用父类的构造方法,再调用子类的构造方法。(简单去记,那对于现实中同样是这样“先有父才有其子”的关系)

    既然基本上是子类成员优先,但是我如果即希望保留子类的成员,又想访问父类的成员呢?

    super 关键字

    super 可以认为是子类内从父类继承来的这块内存的内存的标识。那么就需要用到 super.成员 来在子类去访问父类的成员了。注意只能在子类的非静态成员方法中去访问父类的成员。

    上面对于构造方法的说明中,我们明白需要先在子类对象构造完成之前,帮助父类对其中的成员进行构造。那么此时调用父类的构造方法就需要super() 。如果父类的构造方法是含有参数的,super() 括号内也只管填入参数列表即可。这个语句只能放在子类的构造方法的第一行,且不能和 this 同时出现。

    // User.java文件
    public class User {
        public String name;
        public int age;
    
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
    }
    // Student.java文件
    public class Student extends User {
        public int studentId;
        public String gender;
    
        public Student(String name, int age, int studentId, String gender){
            super(name,age);
            this.studentId=studentId;
            this.gender=gender;
        }
    }

    真的需要每次我们都要在子类的构造方法内第一行写 super() 嘛?其实如果在父类只有一个不带参数的构造方法的情况下,会自动在子类的构造方法第一行前有隐含的 super(); 调用。

    我们会发现这个表达和我们之前文章中讲的this引用时很相似,我们对比下:

    表达方式 / 作用superthis
    关键字.成员子类中访问父类的成员访问当前类对象的成员
    关键字()子类中访问父类的无参构造方法访问当前类对象的无参构造方法

    同样,也有很多共同点:比如第二种表达方式都只能放在方法内的第一行,只能用来访问非静态成员方法和字段。

    代码块执行顺序

    我们知道静态是依赖于类的,所以静态代码块 static {} 是在类加载时执行,即最先被执行的,在 父类和子类的关系上也是如此,父类的静态代码块先于子类的代码块的执行。

    还有种代码块叫实例代码块,是直接用 {} 括起来的(也不带访问修饰限定符)。比如我们无法在类中直接放赋初值语句等,但是可以放到实例代码块内。实例代码块的执行优先级是高于构造方法的执行,原因是给对象分配内存的时候,接下来就是实例化的过程:实例变量→实例代码块→构造方法,而且重要的是实例代码块可能依赖于对象实例变量的状态,但不一定需要构造方法提供的具体参数。

    对于继承关系的两个父子类,那么父类的实例代码块执行,再父类构造方法执行完后才能去执行子类的实例代码块,再执行子类的构造方法。上文我们提到还有静态代码块,如果父类和子类均有静态代码块的话,则顺序依次是:父类静态代码块→子类静态代码块→父类实例代码块→父类构造方法→子类实例代码块→子类构造方法。(顺序简析:父子类被加载,静态代码块先父后子执行,父子类对应的对象被创建后需要实例化,先父后子完成实例化过程)

    protected 访问修饰限定符

    范围private默认protectedpublic
    同个包同个类
    同个包不同类
    不同包内子类
    不同包非子类

    经过本文说明,到这儿就能明白这里的子类是什么意思了。如果被 protected 修饰了成员变量,成员方法等表示要么只能在同一个包中的类中进行访问,要么在不同包中只能通过在继承关系上的子类对象来访问。

    形象地说,protected 就可以看作是一个被保护的“家”,这个房间是家里的人“父与子”还有有关系的亲戚都可以使用的,外部的人如果没有与家中的人有亲属关系是不能访问的。

    结合上一篇文章,我们基本讲完了四大访问修饰限定符,什么时候该用哪种呢?

    我们希望类要尽量做到“封装”,只暴露出必要的信息给类的调用者,而无需关心其内部实现。所以在设计类的时候应该尽可能的使用比较严格的访问权限

    final 限制继承

    final 关键字去放在成员变量的访问修饰限定符前,代表这个变量值已经完全确定,此时表示常量。

    final 关键字去放在类的访问修饰限定符前,代表这个类无法继承了。如我们常用的 String 类。

    组合

    组合也是像继承一样可以代码复用的一种方式。组合是指在新类中创建现有类的对象。

    // 假如这是一个APP的组成
    
    // 导航条
    class NavBar{
    
    }
    
    // 顶部banner
    class Banner{
    
    }
    
    // 内容元素
    class Content{
    
    }
    
    public class App {
        // 这部分就是实现了组合
        private NavBar nav;
        private Banner banner;
        private Content content;
        
    }

    需要区分的是,组合表示出来的是一种「has-a」的关系,而继承则是「is-a」。组合是类A 拥有 类B 的一个实例,A类包含了B类的实例作为其成员变量,来实现“整体”与“部分”之间的关系;继承是子类 父类的一种特殊类型,比如上文中Student是User中的一种特殊用户。

    而且一般推荐是:能用组合的尽量用组合。因为使用继承,不可避免地打破了封装性(违反OOP原则),迫使开发者去了解父类内的细节,而且父类的更新可能导致bug,对程序维护来说并不有利。

    多态

    多态也是字面意思, 不同的对象会有不同的状态。比如同样是叫声,猫叫和狗吠肯定不一样。

    要实现多态有三个必要条件,缺一不可:在继承体系下、子类必须要对父类中的方法进行重写、通过父类的引用调用重写的方法。所以多态的变化是根据方法里传递不同类的对象的时候来体现的。

    方法的重写

    介绍

    之前的文章中有提到过方法的重载,是指同个方法名但是不同的参数列表、返回类型和访问修饰符,这使得同一个方法名可以根据参数的不同实现不同的功能。

    那么,方法的重写是发生在子父类间的同名、非static、非private修饰、非final修饰的方法中,当子类重新定义父类中的方法时,会覆盖父类的方法实现。

    还需要注意:返回值、形参、方法名都是要和父类中的一致(外壳不变,核心重写);子类重写方法的访问权限不能⽐⽗类中被重写的⽅法的访问权限更低;在父类被staticprivate修饰的方法、构造方法都不能被重写。

    在IDEA中,对于重写的方法可以使用 @Override 注解来显示指定,而且IDEA可以识别到这个注解去进行合法性校验。

    综上,其实方法重载是一个类的多态性的体现,而方法重写是子类与父类的一种多态性表现。

    避免在构造方法中调用重写方法

    先说结论:用尽量简单的方式使对象进入可工作状态。尽量不要在构造方法内调用方法。

    如果出现了构造方法内要调用的方法被子类重写了以后,我们走一遍流程就会发现bug:首先,在构造子类对象的时候,要优先构造父类对象由此就会调用父类的构造方法。父类的构造方法内会调用被子类重写的方法,但由于子类对象还未完全初始化,此时调用该方法可能导致异常或未定义行为。

    向上转型

    父类引用指向子类对象,就是表达为 User stu = new Student("nibbles",18,123456,"男"); stuUser 类型的引用变量其指向了其子类 Student 类型的对象。

    这样进行向上转型了虽然无法使用子类的特有的方法,但是可以在方法传参的场景下可以把形参作为父类型引用去接收任意类型的子类对象,也可以作为方法返回值来返回子类对象(如 return new Student(...))。

    如果说有两个父类引用都指向了子类对象,都对父类里的一个方法进行了重写。那么调用该方法时,实际执行的是子类中重写后的方法,这就是动态绑定机制,也是多态性的体现。

    向下转型

    在上述向上转型后,如果还想调用子类特定的方法,此时就需要把父类引用再还原回子类对象。

    package cn.nibbles.animaldemo;
    
    // Animal.java
    public class Animal {
        public String name;
        public int age;
    
        public void eat(){
            System.out.println("eating");
        }
    }
    
    // Cat.java
    public class Cat extends Animal{
        public Cat(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void mew(){
            System.out.println("cat mewing");
        }
    }
    
    // Dog.java
    public class Dog extends Animal{
        public Dog(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void bark(){
            System.out.println("dog barking");
        }
    }
    
    
    // Test.java
    public class Test {
        public static void main(String[] args) {
            Cat cat = new Cat("a cat" , 1);
            Dog dog = new Dog("a dog" , 2);
    
            Animal ani = cat;
            ani = dog;
            // 模拟父类引用变量多次更换对象
    
    //        cat = (Cat)ani;
    //        cat.mew();
            // ClassCastException异常
    
            dog = (Dog)ani;
            dog.bark();
            // 正确向下转型到原子类对象
        }
    }
    

    但是,这里就出现个问题,如果我忘记了之前父类型的引用变量对应的子类型的话,那程序就会出现ClassCastException异常,可以使用 instanceof 运算符在类型转换前进行检查,避免此类异常的发生。

    那么上面的示例代码中向下转型的部分可以改写为:

    // Test.java
    package cn.nibbles.animaldemo;
    
    public class Test {
        public static void main(String[] args) {
            Cat cat = new Cat("a cat" , 1);
            Dog dog = new Dog("a dog" , 2);
    
            Animal ani = cat;
            ani = dog;
            // 模拟父类引用变量多次更换对象
    
            if (ani instanceof Cat){
                cat = (Cat)ani;
                cat.mew();
            }
            // ClassCastException异常 -> 检查不通过、不报异常
    
            if(ani instanceof  Dog){
                dog = (Dog)ani;
                dog.bark();
            }
            // 正确向下转型到原子类对象 -> 正常执行
        }
    }
    

    圈复杂度

    核心思想是用代码中的控制流图环路数量(即独立路径数)来衡量一个程序的复杂度。圈复杂度反映了代码中“决策点”的数量,也就是需要做判断的地方(如 ifwhilefor等)。值越高,说明代码的逻辑分支越多,测试起来更复杂,也更难维护。

    而多态就是可以减少圈复杂度,不必写那么多分支结构。可扩展能力也更强,能直接新增一个新的类去继承父类即可。

  • 包和封装 | Java语言关键

    包和封装 | Java语言关键

    包和封装也是类这部分的知识,类和对象请见-> https://nibbles.cn/java-class-obj.html

    包(Package)是组织类的一种方式,可以防止类名冲突和提高代码的可读性。可以简单看成里面都是装类的文件夹。包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式。

    导入

    包的导入可以使用 import 语句导入,比如导入util包下的Date类可以如下:

    import java.util.Date;
    
    public class Main {
        public static void main(String[] args) {
            Date day = new Date();
        }
    }

    一般来说,我们直接输入类名后,如果是自带在JDK内的,由IDEA会自动帮我们添加。

    如果你也尝试了上面这样在输入 Date 后,会发现出现了两个同名类:

    那么,这就是包的作用,把同名的类放在不同的包中做好区分。而且包名一般都是小写的。如果是java. 开头则代表是Java标准库的包命名空间前缀,util代表是子包名称。

    所以为了防止冲突,最好不要使用 import java.util.*; 这样通配符的形式。或者在创建对象的时候从包到具体类写全:java.util.Date date = new java.util.Date();

    还有一种方式是使用 import static 导入包中的静态的方法和字段,比如下面的二元一次方法求根两种写法:

    public class Main {
        public static void main(String[] args) {
            double a = 1.0;
            double b = 2.0;
            double c = 1.0;
    
            double ret1 = (-b+Math.sqrt(b*b-4*a*c))/(2.0*a);
            double ret2 = (-b-Math.sqrt(b*b-4*a*c))/(2.0*a);
    
            System.out.println(ret1);
            System.out.println(ret2);
        }
    }
    import static java.lang.Math.*;
    
    public class Main {
        public static void main(String[] args) {
            double a = 1.0;
            double b = 2.0;
            double c = 1.0;
    
            double ret1 = (-b+sqrt(b*b-4*a*c))/(2.0*a);
            double ret2 = (-b-sqrt(b*b-4*a*c))/(2.0*a);
    
            System.out.println(ret1);
            System.out.println(ret2);
        }
    }

    自定义包

    右键src文件夹创建的时候选择Package即可。一般的包名会使用域名的颠倒形式(如 cn.nibbles.mypackagename )包名要和文件路径匹配(如创建的cn.nibbles.mypackage 包的文件夹目录为cn/nibbles/mypackage)。

    包内的类文件顶部都会通过package关键字指定路径:

    package cn.nibbles.mypackage;
    
    public class NiBu {
        public String name;
        private int age;
    
        public nibu(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public int getAge() {
            return age;
        }
    }
    

    访问权限控制

    在定义类的时候,如果没有指定成员变量/方法的访问权限,则默认(什么都不写)就是包访问权限。同样是权限的关键字还有:privatepublicprotected 访问权限的用途就是控制方法或者字段能否直接在类外使用。

    如上我们已经创建好的 NiBu 类,如果在其他地方访问,那么成功导入类和创建对象后,只有name是可以被访问到的,age是只能在类内使用,如果不写则是不允许被其他包内的类访问。

    范围private默认protectedpublic
    同个包同个类
    同个包不同类
    不同包内子类
    不同包非子类

    protected 涉及到继承,在后续会说到

    封装

    面向对象程序三大特性:封装、继承、多态。接下来就是对封装概念引入。

    封装的东西大家在平时生活中也总是用到。比如对于芯片,我们直接看一般就是个黑色的片状还带有引脚,主要通过引脚与电路交互,我们只需要知道各引脚的功能即可。对于一台装好的主机,我们直接看就是个机箱还带有一些接口,我们只用插好对应的外设和插头即可使用,无需关注内部元件之间的线路走向等。

    封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

    适当的封装,可以将对象使用接口的程序实现部分隐藏起来,不让用户看到,同时确保用户无法任意更改对象内部的重要资料,若想接触资料只能通过公开接入方法(Publicly accessible methods)的方式( 如:”getters” 和”setters”)。
    ——封装 (面向对象编程)(Wikipedia

    那么,其实看完这里的说明,你或许发现了,我们从前一篇文章的Getter和Setter,到现在private访问权限的讲解,都是为了封装所服务的。

    不直接通过引用来访问其中的数据,而使用公开的作为接口的方法来进行访问。隐藏并保护了对象的内部实现细节,保护数据不被意外修改或破坏。这些都是封装的作用。

    static 关键字

    Java中被static修饰的成员成为静态成员,那么成员变量和成员方法都有静态和非静态的两种。而静态成员也被称为类成员,其不属于某个具体的对象,是所有对象所共享的。静态依赖于类,非静态依赖于对象。

    对于“共享”的理解,比如我只需要做这一个学校的记录学生信息的学生类,大家都是一个学校所以类内的代表学生所在学校的成员变量就可以是静态成员变量,所有的对象都是一个学校的值。

    package cn.nibbles.mypackage;
    
    public class StudentInfo {
    
        public static String school;
        public String name;
        public int age;
        public String className;
    
    }
    

    静态成员变量

    如上,school 就是一个静态成员变量(也成为类变量),有几点特性:首先就是,类的属性,所有对象共享的;既可通过对象访问还可以通过类名访问(更推荐后者);类变量存储在内存的方法区;类变量的生命周期同类(随着类的加载而创建,随类的卸载而销毁)

    静态成员变量的访问可以通过 “类名.静态成员变量名”,如上就是通过 StudentInfo.school 访问。当然访问还是要遵循访问控制的关键字划分的权限。

    静态成员变量的初始化,除了直接的就地方式 public static String school="Nameless" ,还有种方式就是静态代码块初始化。代码块的概念很简单,就是用 { } 扩起来的。静态代码块形如

    package cn.nibbles.mypackage;
    
    public class StudentInfo {
    
        public static String school;
        public String name;
        public int age;
        public String className;
        
        static {
            school = "Nameless";
        }
    
    }
    

    但是静态成员变量一般不会放在构造方法中来初始化,构造方法来初始化的只是跟对象有关的。但如果非要这么做,则需要静态的Setter。

    静态成员方法

    同样的,被static修饰的成员方法称为静态成员方法,是类的方法。所以也有几个特性:推荐使用“类名.静态方法名()”方式调用,如 StudentInfo.getSchool() 这样;不能在静态方法中访问任何的非静态成员变量,也不能在静态方法中调用任何非静态方法,原因是非静态是依赖于对象,就像是非静态方法内有this参数是无法知道引用的对象的。

  • 类和对象 |  Java语言关键

    类和对象 | Java语言关键

    在之前文章中已经入门简单的语法,那么接下来将讲到Java中很重要的概念——面向对象。

    在Java的Wikipedia中多范式(Paradigm)的条目中是这样解释的:

    genericobject-oriented (class-based)functionalimperativereflectiveconcurrent
    通用、面向对象(基于类)、函数式、命令式、反射式、并发

    介绍

    是一种创建对象的图纸,对象是根据所创建的实例。

    可以在类中定义好属性(描述其的变量)和方法(可以执行的操作)。而根据类来创建的对象,也就具有了这些属性和可使用的方法。如果要创建多个同类对象,只需要根据同一类来创建这多个对象即可,这样大大方便了代码的复用与维护。

    面向对象(OOP)可以看作为是一种编程思想,指的是利用对象之间的交互去实现功能。

    这样以面向对象的形式来组织代码,就可以做到比如引入外部的类,直接使用方法和属性,而且无需知道和细究方法内部的具体实现,只需要知道方法能做到的功能即可。

    对一个对象的重新认识的一个过程就是对对象进行抽象。

    类的定义

    public class Demo {
        public int number;
        public void out(){
            System.out.println("OUT");
        }
    
    }

    如上,一个类的定义包含:

    • 类的名字,一般我们使用大驼峰命名法(如ClassName),名字中每个英文都是大写。
    • 成员变量
    • 成员方法

    一般建议,一个文件中定义一个类。 且public修饰的类名必须要和文件名相同,而且这样的类名最好通过IDE的内置工具修改。

    你可能会感觉到这和C语言的结构体很像,但是与之不同的是,Java的类是面向对象的,且包含了成员变量和成员方法,支持访问控制修饰符(public/private等),并且还有继承、多态等多种机制。这些会在后续展开。

    类的实例化=对象

    用类类型创建对象的过程,称为类的实例化。实例化的代码:

    public class Test {
        public static void main(String[] args) {
            Demo obj = new Demo();
            obj.out();
        }
    }

    这样如第三行所示的格式,就是一个创建对象的语句。(关键字是new , 所以常有人说“new一个对象”)

    而创建好的名为obj的对象就是根据名为Demo的类创建的。之后我访问了Demo类中的成员方法(见第四行),访问或修改类中的成员一般使用点号( . ),这样就成功输出了“OUT”

    这样从内存空间存储的角度上,类这个“图纸”其实并不会占用程序运行时候用的堆内存,只是作为一种设计蓝图,一种元数据被存储在方法区,而根据类创建出对象才是真正会开辟堆内存空间的,所以这样也就更好地理解了为什么创建对象能被称为“实例化”。

    this引用

    在成员方法内,当出现方法中定义的形参和成员变量相同的时候,就需要用this+点号( this. )来表示指明是这个对象的成员变量。

    类似的,前面说到可以一个类可以创建多个对象,那么试想在这个类中有负责传入参数的成员方法,程序如何知道传给哪个对象相应的数据?同样也是借助了 this 对当前对象的引用。

    回到前面,讲讲如何对这个对象内的成员变量进行赋值。

    如果只是类内对成员变量指定初始值的话直接就是一条 public int yes = 1;即可,但是不可以拆开来两条去写,如public int yes; yes = 1; 。这是因为对于类内只能有“成员的声明”和“初始化块”,而前条语句会被判为初始化,而后者会被认为是普通语句不可使用。

    如果对于对象,不同的对象希望有不同的变量值的话,则需要写修改器,这也是种成员方法。而且对于IDEA很简单,可以直接借助内置工具生成。在类的文件内右键选择 Generate… (ALT+Insert快捷键) -> 选择 Setter, 然后在弹出的窗口中选中需要使用的修改器传入值的变量。则编辑器会直接生成如下的的代码:

    public void setNumber(int number) {
        this.number = number;
    }

    你会发现,在第二行好像自带了一个 this ,没错,就是在这里做到了让程序知道值应该传递给哪个对象。只要哪个对象调用了这个方法,那么这个 this 就是代指哪个对象。只能引用当前对象,不能再引用其他对象。而且有的时候编译器会在成员方法会被执行的时候将其自动传递。

    看一个自动传递的例子,还有种方法叫做访问器,这也是种成员方法。还是右键,Generate… (ALT+Insert快捷键) -> 选择 Getter,然后在弹出的窗口选择需要输出的变量。就会生成一个我们很熟悉的用于打印变量的方法:

    public void numOut(){
        System.out.println(number);
    }
    // 一般直接生成的是上面这样的
    
    public void numOut(){
        System.out.println(this.number);
    }

    其实简单改改自动生成的方法如第二段所示的,会发现结果也是一样。而这种情况即使不写,编译器也会自动加上 this

    构造方法

    也成为构造器,也是一个特殊的成员方法。名字必须与类名相同,在创建对象的时候由编译器自动调用,并且在整个对象的生命周期内只使用一次。

    // 文件Student.java中
    public class Student {
        public String name;
        public int age;
        public String gender;
    
        // 构造方法
        public Student(String name, int age, String gender) {
            this.name=name;
            this.age=age;
            this.gender=gender;
        }
        public Student(){
            this.name="nameless";
            this.age=0;
            this.gender="boy";
        }
    
    }

    需要注意的几点是:无需设置返回值也不能设置,可以重载(即同一方法可以按照使用场景设置不同的参数,如上无参数的构造方法,可以作为指定成员变量的默认值的一种方式)

    如果开发者没有显式定义(如上直接在类中写出构造方法即为显式定义),则编译器会默认生成一个不带参数的构造方法。如果有则不再额外生成。

    如果只是无参来指定默认值的话,上述无参的构造方法还可以用this简化为:

    public class Student {
        public String name;
        public int age;
        public String gender;
    
        // 构造方法
        public Student(){
            this("nameless",0,"boy");
        }
        public Student(String name, int age, String gender) {
            this.name=name;
            this.age=age;
            this.gender=gender;
        }
        
    }

    注意,使用this()调用其他构造方法时,必须将该调用语句放在构造方法的第一行

    绝大多数情况下使用public来修饰,特殊场景下会被private修饰(单例)

    对象打印

    public class Main {
        public static void main(String[] args) {
            Student stu = new Student();
            System.out.println(stu);
        }
    }

    用上面的代码直接打印会输出 Student@3b07d329 这样的“类名@hash值”的格式。

    但是如果想要打印对象中的成员变量,可以使用类中重写的 toString 方法。因为在上面的例子中,对于对象是直接进行了“sout”打印,实际上在类中是在调用一个默认的 toString 成员方法。

    IDEA也可以生成一个简单的重写方法,右键,Generate… (ALT+Insert快捷键) -> 选择 toString -> 选择要打印的成员变量即可。

    public class Student {
        public String name;
        public int age;
        public String gender;
    
        public Student(){
            this("nameless",0,"boy");
        }
        
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender='" + gender + '\'' +
                    '}';
        }
    }

    如果希望自己写 toString ,记得加上 @Override 注解确保正确重写。

    之后会继续讲包、封装、还有继承、多态等,虽然在本文有部分是提到但暂不细讲,循序渐进。

  • 运算符 | Java语言基础

    运算符 | Java语言基础

    运算符 不仅是 Java ,在众多编程语言中基本上都是相通的,学会迁移

    Java中运算符可分为以下:算术运算符(基本+ - * /,增量+= -= *= %=,自增++/自减--)、关系运算符(< > == !=)、逻辑运算符(&& || ! & |)、位运算符(& | ^ ~ )、移位运算符(<< >> >>>)以及条件运算符等。

    算数运算符

    算数异常

    做除法和取模的时候右操作数不能为0,否则会出现“除0异常”

    public class Test {
        public static void main(String[] args) {
            int a = 20;
            int b = 0;
            System.out.println(a/b);
        }
    }

    这里 ArithmeticException 代表这里是一个算数异常的结果,后面 / by zero代表 “除0” ,第二行是定位错误在哪一行。

    取模规则

    public class Test {
        public static void main(String[] args) {
            int c = 7;
            int d = 3;
            System.out.println( c %  d); // 1
            System.out.println(-c %  d); //-1
            System.out.println( c % -d); // 1
            System.out.println(-c % -d); //-1
        }
    }
    • Java 中 % 运算符的规则是:余数的符号与被除数相同。
    • 对于正数,余数就是通常的”除法余数”。
    • 对于负数,余数会按照规则调整,以确保余数的符号与被除数一致,且余数的绝对值小于除数的绝对值。

    而且在Java语言中取模支持运算符两边均为浮点数,和C语言两边均为整数不同。

    public class Test {
        public static void main(String[] args) {
            System.out.println(25.5%2.0); // 1.5
        }
    }

    自增运算符

    如果num为一个已定义的整型变量,形如 num++++num 的表达式中 ++ 代表自增,同理,-- 代表自减。

    如果单独使用这两个效果一样。都是表示为直接+1。而混合使用则分为两种:

    • 前置 ++ :先给变量+1,然后使用变量的值;
    • 后置 ++ :先使用变量原来的值,再表达式结束后给变量+1;
    public class Test {
        public static void main(String[] args) {
            int a = 10;
            System.out.println(a++); // 11
            System.out.println(a);   // 12
        }
    }
    public class Test {
        public static void main(String[] args) {
            int a = 10;
            System.out.println(++a); // 12
            System.out.println(a);   // 12
        }
    }

    关系运算符与逻辑运算符

    C语言中也有相通的《从猜数字程序来理解结构和随机数 – 3.1 Intro》

    关系运算符有> < != == >= <=,返回 truefalse.

    同样的,和C语言一样,形如 0<a<9 多次判断是不能连着写的(已定义整型变量a),要写成 a>0 && a<9 。因为原来的错误写法,如果判断的话,假设 a>0 结果确实是 true 了,但是后面判断就是 true<9 这样是无法比较的。

    public class Test {
        public static void main(String[] args) {
            int a=10;
            System.out.println(a>10);  // false  正确写法
            //错误写法: System.out.println(1<a<11);
            System.out.println(a>1 && a<11); // true 正确写法
            System.out.println(a>10!=true); // true 正确的连续判断写法
        }
    }

    逻辑运算符有与&& (也有单&)或|| (也有单|)非! ,注意与和或操作两边的表达式是结果为boolean类型的表达式。

    • 与:有false出false,全true才true
    • 或:有true出true,全false才false
    • 非:即对立,!true==false !false==true

    逻辑运算符的短路求值

    短路求值就是提前判定,遵循着上述的“有x出x”。

    • 对于 && , 如果左侧表达式值为 false, 则总表达式结果⼀定是 false, ⽆需计算右侧表达式.
    • 对于 || , 如果左侧表达式值为 true, 则总表达式结果⼀定是 true, ⽆需计算右侧表达式.

    所以这样的话,如果左侧表达式值能直接影响整个表达式结果的话,那么提前“短路”。这样即使右边表达式出错,也仍可以输出整个表达式的值。

    public class Test {
        public static void main(String[] args) {
            System.out.println(10 > 11 && 10 / 0 == 0);  // 左侧为false,与逻辑中有false出false,所以提前短路
            System.out.println(10 < 11 && 10 / 0 == 0);  // 左侧为true,那么结果要取决于右侧,右侧表达式错误,所以抛出异常
        }
    }

    如果不希望短路求值,可以使用单 & 表示与和单 | 表示或。 但一般用得频率也比较少。

    位运算符

    Java 中数据存储的最⼩单位是字节,⽽数据操作的最⼩单位是位(bit)。

    位运算符有四个 & | ~ ^,只有 ~ 是一元运算符。

    位操作表⽰按⼆进制位运算,按位运算就是在按照⼆进制位的每⼀位依次进⾏计算。

    按位与、按位或、按位异或

    对于 ^ 异或来说:那就是相同为0,不同为1。(可以想象成“找不同”)

    对于 ~ 按位取反操作来说,可以说是把二进制写出来后逐位取反。

    public class Test {
        public static void main(String[] args) {
            System.out.println(0xa); // 10
            System.out.printf("%x\n",0xa);  //a,以十六进制输出
            System.out.printf("%32s\n",Integer.toBinaryString(0xa)); // 00000000000000000000000000001010
            System.out.println(~0xa);  // -11
            System.out.printf("%x\n",~0xa);  //fffffff5,以十六进制输出
            System.out.printf("%s\n",Integer.toBinaryString(~0xa));  // 11111111111111111111111111110101
        }
    }

    移位运算符

    移位运算符有三个,<< 左移,>> 右移,>>> 无符号右移 (不存在无符号左移)。不常用,了解即可。对于一个正数来说,有如下规律

    • 左移:写出二进制并左移且右边补0,结果为 <原值>*2的<位数>幂次方
    • 右移:写出二进制并右移左边补符号位,结果为 <原值>/2的<位数>幂次方
    • 无符号右移:写出二进制并右移左边始终补0

    条件运算符

    格式 : <条件>?<真时执行>:<假时执行> 同C语言中的。

    运算符优先级

    不好记忆的话,那就加上括号调整优先级吧!

  • 变量详解 | Java语言基础

    变量详解 | Java语言基础

    基本数据类型表

    在前文有提到:《初识Java及数据类型 | Java语言基础》

    基本类型变量通用

    通用格式:<数据类型关键字> <变量名>=<值>

    public class Test {
        public static void main(String[] args) {
            long d = 10L;
            System.out.println(d);
    
            short a = 10;
            System.out.println(a);
    
            byte b = 10;
            System.out.println(b);
    
            float c = 10.24f;
            System.out.println(c);
        }
    }
    • 对于长整型的值来说,需要在数字的末尾加上L(也可以小写,但是大写的方便区分)才能被IDE识别为 long 类型。
    • 对于单精度浮点型的值,需要在数字末尾加上f(也可以大写)才能被默认判断为单精度 float 类型。

    通常,如果查看数据类型可以接受的范围的最大值和最小值,可以利用包装类型的MAX_VALUEMIN_VALUE。一般的,除了 int 的包装类型名为 Integer ,其他的是关键字首字母大写即包装类型名。

    包装类型是基本数据类型中对应的才有的。

    public class Test {
        public static void main(String[] args) {
            System.out.println(Long.MAX_VALUE);
            System.out.println(Long.MIN_VALUE);
            
            //etc.
        }
    }

    整数除法和实数除法

    public class Test {
        public static void main(String[] args) {
            int a = 1;
            int b = 2;
            System.out.println(a/b);
        }
    }

    如上代码输出为0,显然与我们理解的1/2=0.5不同,原因在于除号两边都是整数,相除程序认为就是整数除法,结果自然变成了向下取整的0了。

    如果希望输出预期的0.5这样的浮点数,可以把 a b 变量声明的数据类型改为 double。当然也可以把 a/b 改为 1.0*a/b

    浮点数表示得不精确?

    public class Test {
        public static void main(String[] args) {
            double a = 1.1;
            System.out.println(a*a);
        }
    }

    如上代码输出为1.2100000000000002 。 为什么小数点后面会这么长呢?是因为浮点数本来就不是一个精确的数字,计算机也无法表示精确是因为计算机以二进制的方式存储浮点数时,内存布局遵守 IEEE 754 标准(和C语⾔⼀样),尝试使⽤有限的内存空间表⽰可能⽆限的⼩数,势必会存在⼀定的精度误差。

    字符型可以存的字符

    不同于C语言中字符型变量只能存一个单字母,Java语言中的字符型不仅可以存储字母,还可以存储中文的单字。同样的,都需要单引号 '' 把字符括起来。

    在Java语言中字符的底层是Unicode,其中也包含了ASCII的部分,且码值原样不变。也正是底层为Unicode字符集,所以能表示的字符比C语言更多并包含中文。

    字符型的包装类型名是 Character ,但是其MAX_VALUEMIN_VALUE 都是不可打印的字符。

    字符型在计算机中本质上是个整型,可以用如下代码检验:

    public class Test {
        public static void main(String[] args) {
            int ch2int = 'a';
            System.out.println(ch2int);
    
            char int2ch = 'a';
            System.out.println(int2ch);
        }
    }

    布尔型的严格定义

    public class Test {
        public static void main(String[] args) {
            boolean yes = true;
            boolean no = false;
        }
    }

    如上 yes 和 no 两个变量可以赋的值只能是布尔值中的真( true )假( false ),Java语言中没有隐式地将数字(隐式就是如C语言的”非零为真,0为假”)转换为布尔值

    Java虚拟机规范中,并没有明确规定boolean占⼏个字节,也没有专⻔⽤来处理boolean的字节码
    指令,见官方文档的引用:

    「Chapter 2. The Structure of the Java Virtual Machine – The boolean Type」
    In Oracle’s Java Virtual Machine implementation, boolean arrays in the Java programming language are encoded as Java Virtual Machine byte arrays, using 8 bits per boolean element.

    类型转换

    自动类型转换(隐式)

    代码不需要经过任何处理,在代码编译时,编译器会⾃动进⾏处理。整数默认为int整型,浮点数默认为double双精度。

    特点:数据范围⼩的转为数据范围⼤的时会⾃动进⾏。

    比如让一个long类型的变量赋值为int类型变量:

    public class Test {
        public static void main(String[] args) {
            int a = 10;
            long b = a;
            System.out.println(b);
        }
    }

    强制类型转换(显式)

    当进⾏操作时,代码需要经过⼀定的格式处理,不会自动处理。

    如果数据范围大的类型转化为数据范围小的,利用强转出现两种情况:如果范围大的类型的数字本来就在范围小的类型内的话,其实截断后没有影响;但是范围大的类型的数字还大又超出了范围小的类型可接受的数值范围,则会造成丢失数据(或者说精度丢失),但仍可输出(IDE也会有检查操作),因为你的强转操作本身就代表了你接受丢失数据的后果。

    不相⼲的类型不能互相转换。

    类型提升

    不同类型的数据之间相互运算时,数据类型⼩的会被提升到数据类型⼤的。

    public class Test {
        public static void main(String[] args) {
            int a = 10;
            long b = 256l;
            long c = a + b;   // yes
            int d = a + b;  // no
        }
    }

    但是也有个同类型运算出现的奇怪现象,这个类型就是byte

    public class Test {
        public static void main(String[] args) {
            byte a = 10;
            byte b = 1;
            byte c = a + b;   // no
            int d = a + b;   //yes
        }
    }

    虽然a和b都是byte类型,但是计算时候会把a和b均自动提升为int,导致结果也需要int。这是由于计算机CPU是通常按照4个字节为单位从内存中读写数据,以此为了硬件上实现的方便。short类型也同理。

    字符串类型 – 引用数据类型

    字符串的关键字为 String ,注意大写。

    字符串拼接与字符串和整数互转

    两个字符串相加的效果就是字符串拼接。

    public class Test {
        public static void main(String[] args) {
            String str = "Hello";
            String str2 = " world!";
            System.out.println(str+str2);
        }
    }

    有时需要整数和字符串拼接,即使单个整数加入到字符串可以采用 System.out.println("a =" + 10); 但是如果希望加号后面是一个表达式呢? 可以对表达式加个括号,System.out.println("a =" + (a+b));

    又或者可以使用String类类型的功能:String str = String.valueOf(<数字或结果为数字的表达式>); 使得整数转为字符串。

    反过来,可以使用 int num = Integer.parseInt(<数字字符串>);

    public class Test {
        public static void main(String[] args) {
            int a = 10;
            String str = String.valueOf("a = " + a);
            System.out.println(str);
            int b = 100;
            String str2 = String.valueOf("a + b = " + (a + b));
            System.out.println(str2);
    
            String stra = "10";
            int a2 = Integer.valueOf(stra);
            System.out.println(a2-1);
        }
    }
  • 初识 Java 及数据类型 | Java语言基础

    初识 Java 及数据类型 | Java语言基础

    之前发过一期简单的入门《 Java 上的HelloWorld,怎么这么多细节?》 ,接下来将会是系统地学习Java之旅~

    JavaSE & JavaEE

    Java Standard EditionJava Enterprise Edition
    开发桌面应用和简单服务器程序用于开发大型、分布式企业应用和Web应用
    核心语言特性和基本API(如I/O、网络、GUI)企业级API(如Servlet、JSP),特性更广泛、复杂
    适合小型和中型应用适合大型应用
    可在标准JVM上运行需要特定的应用服务器

    Java的发展历史

    Java之父 —— 詹姆斯·高斯林(James Gosling),Sun 公司(后被Oracle收购)

    1995年PC互联网时代,5月时以Java名称发布,提出“Write once, run anywhere.”口号。

    能实现跨平台的主要原因就是编译后的 *.class 文件,可以跨平台运行。

    JDK安装和后续文章环境说明

    • 环境:JDK安装路径不要有中文,以后装软件里面也不要有中文。如果是多个Java版本请设置PATH指定命令行中使用的开发环境版本(如果是IDE内,直接软件内切换即可)
    • 集成开发环境IDE:IDEA Community 2022.1.4(社区版)免费,软件寻找:Bing搜索idea→IntelliJ IDEA→“下载”按钮→软件图片右下角“其他版本”→看第二列Community部分的,下拉找到2022.1.4中 2022.1.4 – Windows x64 (exe)
    • 后续文章中的环境(如果没有特殊说明的话):
      Windows 11、 JDK17(17.0.9)、IDEA Community 2022.1.4
    • 如果向他人请教Java编程中的疑问,请先附上自己的环境~

    PATH配置路径:(Windows 11)右键任务栏的开始选择设置→系统→下拉到最后找到设备信息→相关链接中的高级系统设置→高级选项卡中最下面的环境变量,双击其中的Path进入“编辑环境变量”窗口,新建 %JAVA_HOME%\bin; (其中博主右多个Java版本所以加了数字进行区分方便切换环境,新建的这个路径格式应该是 %变量名%\bin ,还需在“环境变量”窗口下再点击一次“新建…”从而新建名为JAVA_HOME的变量,值为JDK安装的路径)

    环境变量设置

    HelloWorld I/O

    更多可以联系前文:《Java上的HelloWorld,怎么这么多细节?》

    流程

    ① 写代码 → 保存为 HelloWorld.java

    public class HelloWorld{
        public static void main(String[] args) {
            System.out.println("HelloWorld");
        }
    }

    ② cmd内编译 (javac.exe)→ 生成字节码文件 HelloWorld.class

    javac HelloWorld.java

    ③ cmd内执行输出

    java HelloWorld

    运行在JVM(Java虚拟机)上,包含在JDK中。

    【面试考法】JDK、JRE、JVM区别?

    • JDK:Java开发⼯具包(Java Development Kit),提供给Java程序员使⽤,包含了JRE,同时还包含了编译器javac与⾃带的调试⼯具Jconsole、jstack等。
    • JRE:Java运⾏时环境(Java Runtime Environment),包含了JVM,Java基础类库。是使⽤Java
      语⾔编写程序运⾏的所需环境。
    • JVM:Java虚拟机,运⾏Java代码
    Java IDE、JDK、JRE、JVM各层级关系

    各部分概念带过

    public 修饰符class 定义类的关键字Test 类名
    publicstatic 声明类的静态成员void main(String[] args) 方法

    命令行参数 String[] args

    用一个测试案例说明可以借助该参数输入一些值:

    public class HelloWorld{
        public static void main(String[] args) {
            for (int i=0;i<args.length;i++){
                System.out.println(args[i]);
            }
        }
    }
    java HelloWorld nibbles hello world!
    // 输出:
    // nibbles
    // hello
    // world

    Java代码文件的层级

    代码层级示意图
    • class 是类,在⼀个代码⽂件中只能有⼀个public修饰的类,⽽且代码⽂件名字必须与public修饰的类名字相同。
    • 方法即为函数,main() 依然是程序的入口
    • 代码规范:注意左花括号的位置

    注释

    //单行注释
    /*多行注释*/ 
    /** ⽂档注释 */ 
    
    /**
    文档注释:
        @version v1.1.0
        @author nibbles
        HelloWorld practice
    */
    //javadoc命令用于生成文档注释
    javadoc -d myHello -author -version -encoding UTF-8 -charset UTF-8 HelloWorld.java

    字符集不对的报错

    cmd报错截图

    如果说需要在注释中或输出中包含中文,需要保持文件编码和cmd命令行编码的统一,否则会在编译时候出现如上错误。比如都设置为GBK,见下:

    如何查看cmd窗口的代码页编码?

    右键cmd窗口标题栏,属性→当前代码页

    不推荐更改代码页编码,其更改注册表比较繁琐。文件转编码其实更快。

    如何按指定编码保存?

    以VSCode编辑器为例,

    或者在写编译命令时,加上encoding参数 (表示文件是以UTF-8保存的,那么编译时候解码就如此)

    javac HelloWorld.java -encoding utf-8

    使用IDE就可以避免这个情况。

    注释规范

    1. 内容准确:注释内容要和代码一致匹配,并在代码修改时及时更新
    2. 篇幅合理:注释既不应该太精简,也不应该扯车轱辘话长篇大论
    3. 使用中文:一般中国公司都要求使用中文写注释,外企另当别论
    4. 积极向上:注释中不要包含负能量

    标识符

    给类名、⽅法名或者变量所取的名字。标识符中可以包含:字⺟、数字以及 下划线和 $ 、_ 符号等(不可以加冒号)。但不能以数字开头,也不能是关键字,且严格区分⼤⼩写。

    驼峰命名建议

    • 类名:每个单词的⾸字⺟⼤写(⼤驼峰) ,如 Test
    • ⽅法名/变量名:⾸字⺟⼩写,后⾯每个单词的⾸字⺟⼤写(⼩驼峰),如 studentName

    未来就业时候视入职公司的发的代码规范/浏览的之前的代码而定,一学代码规范二学业务。

    关键字

    由Java语言提前定义好的,有特殊含义的标识符或保留字。

    各用途关键字

    IDEA高效开发

    快捷补全语句(输入后按tab)

    • psvmmainpublic static void main(String[] args){}
    • soutSystem.out.println()

    运行 (点击框选的 即可)

    IDEA截图

    数据类型

    打印语句和字面常量

    public class Test {
        public static void main(String[] args) {
    
            // 输出123后自动换行,println或print可以接受任意类型
            System.out.println(123);
    
            // 去掉ln则输出末尾不换行
            System.out.print(123);
    
            // 类似于C语言中的printf("%d\n",123);格式化字符串的写法。【用得少】
            System.out.printf("%d",123);
        }
    }

    字面常量概念同其他语言,就是固定不变的量,分字符串常量(双引号””括起来的)、整型常量、浮点数常量、字符常量(单引号”括起来的)、布尔常量(true或false)、空常量(null)。

    数据类型

    字符串、整型、浮点型、字符型以及布尔型,在Java中都称为数据类型。

    八种数据类型的关键字、内存占用、范围

    附注:

    • 内存占用的字节数不因电脑操作系统的32位还是64位的改变而改变
    • 没有像C语言一样分有符号还是无符号的概念,正负均可表示

    变量

    同C语言中的变量定义方法。

    public class Test {
        public static void main(String[] args) {
            //<数据类型关键字> <变量名>=<值> 
            int num = 10;
            System.out.println(num);
        }
    }

    尤其注意要把局部变量(定义在方法内的变量叫局部变量)还要初始化,不能简单的一个 int num; ,否则会导致编译就报错

    对于int类型的变量,四个字节中最高位是符号位(1bit),0为正,1为负。后面31个bit(位)才真正代表数值大小。所以如果对于1字节内7bit数值位的话,可以表示范围也就是 -2^7~(2^7-1)即-128~127。

    则对于-128 转化为 二进制源码为 1 1000 0000;反码为 1 0111 1111(符号位不变);补码(=反码+1)为 1 1000 0000。

    范围验证:

    public class Test {
        public static void main(String[] args) {
            System.out.println(Integer.MAX_VALUE); // Integer属于包装类型,此语句可以输出int类型的最大值
            System.out.println(Integer.MIN_VALUE); // 此语句可以输出int类型的最小值
        }
    }

    在直接赋值常量值的情况下,如果给变量的值超出类型本身可接受的范围,也会在编译时候直接报错。

  • Java上的HelloWorld,怎么这么多细节?

    Java上的HelloWorld,怎么这么多细节?

    课程链接见下(不是推广,只是优秀的课程应该被广泛传播。)

    本文就只列了一些课程重点和我踩过的一堆坑,新手学Java,我们相互学习交流。

    Java的跨平台性

    核心机制(JVM):

    1)JVM是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令管理数据、内存、寄存器,包含在JDK中。
    2)对于不同的平台,有不同的虚拟机。
    3)Java虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”(跨平台)。

    编译和运行流程:

    注意命令!javac是编译,java才是运行。*.java为源文件,*.class为字节码文件。

    若程序无错是无任何提示,默默地在同目录下生成字节码文件。但是有错会报错在cmd,无法编译。

    运行的本质是把字节码文件装载到JVM虚拟机内执行。

    修改后的源文件需要重新编译,才能在再执行中生效。(并非前端的所改即得)

    Hello World!

    public class Hello {
        public static void main(String[] args) {
          System.out.println("Hello World!");
      }
    }

    细节,注意!有的是真实踩过的坑,有的是老师的提醒。

    • print后面的字母是小写的L,即l。意为line“(行),在输出之后会自动添加一个换行符(\n。若不慎打成了printIn则会报错提示找不到这个方法。Java语言严格区分大小写
      有关打印到屏幕上的方法就System.out.println()System.out.print()
    • main(){}间的空格是为了编程规范,程序能跑,但适当的空格有利于协作者的阅读。
    • 但是Java应用程序的执行入口是main()方法。
      它有固定的书写格式public static void main(String[] args)(…)
    • 每条花括号内的语句的后面要以;分号结束,凡是括号都是成对出现的。
    • public class后的类名要和文件名保持一致(因为Java类 Hello 被声明为 public
    • 注意在编译时cmd窗口的代码页编码和代码编辑器中显示的文件编码是否相同(以下可展开)
    • 若输出内容包含中文,务必要设置中文编码GBKUTF-8
    如何查看cmd窗口的代码页编码?

    右键cmd窗口标题栏,属性→当前代码页

    不推荐更改代码页编码,其更改注册表比较繁琐。文件转编码其实更快。

    如何按指定编码保存?

    以VSCode编辑器为例,

    执行命令依次为

    javac Hello.java
    java Hello

    注意后面的指令不用带.class后缀,因为类的名字就是Hello

    其他Tips

    • 编译后,源文件中每一个类都会对应每一个字节码文件,对应依据就是同名
    • 【再提一下】如果源文件包含一个public类则文件名必须按该类名命名!
    • 一个源文件中最多只能有一个public类。其它类的个数不限。也可以将main方法写在非public类中,然后指定运行非public类,这样入口方法就是非public的main方法。main可以在不同类中。