静态变量和静态方法

前面我们已经了解了类的大部分特性,一个类可以具有多种属性、行为,包括对象该如何创建,我们可以通过构造方法进行设定,我们可以通过类创建对象,每个对象都会具有我们在类中设定好的属性,包括我们设定好的行为,所以说类就像是一个模板,我们可以通过这个模板快速捏造出一个又一个的对象。我们接着来看比较特殊的静态特性。

静态的内容,我们可以理解为是属于这个类的,也可以理解为是所有对象共享的内容。我们通过使用static关键字来声明一个变量或一个方法为静态的,一旦被声明为静态,那么通过这个类创建的所有对象,操作的都是同一个目标,也就是说,对象再多,也只有这一个静态的变量或方法。一个对象改变了静态变量的值,那么其他的对象读取的就是被改变的值。

java 复制代码
public class Person {
    String name;
    int age;
    String sex;
    static String info;    //这里我们定义一个info静态变量
}

我们来测试一下:

java 复制代码
public static void main(String[] args) {
    Person p1 = new Person();
    Person p2 = new Person();
    p1.info = "杰哥你干嘛";
    System.out.println(p2.info);   //可以看到,由于静态属性是属于类的,因此无论通过什么方式改变,都改变的是同一个目标
}

所以说一般情况下,我们并不会通过一个具体的对象去修改和使用静态属性,而是通过这个类去使用:

java 复制代码
public static void main(String[] args) {
    Person.info = "让我看看";
    System.out.println(Person.info);
}

同样的,我们可以将方法标记为静态:

java 复制代码
static void test(){
    System.out.println("我是静态方法");
}

静态方法同样是属于类的,而不是具体的某个对象,所以说,就像下面这样:

image-20220920234401275

因为静态方法属于类的,所以说我们在静态方法中,无法获取成员变量的值:

image-20220920235418115

成员变量是某个具体对象拥有的属性,就像小明这个具体的人的名字才叫小明,而静态方法是类具有的,并不是具体对象的,肯定是没办法访问到的。同样的,在静态方法中,无法使用this关键字,因为this关键字代表的是当前的对象本身。

但是静态方法是可以访问到静态变量的:

java 复制代码
static String info;

static void test(){
    System.out.println("静态变量的值为:"+info);
}

因为他们都属于类,所以说肯定是可以访问到的。

我们也可以将代码块变成静态的:

java 复制代码
static String info;

static {   //静态代码块可以用于初始化静态变量
    info = "测试";
}

那么,静态变量,是在什么时候进行初始化的呢?

我们在一开始介绍了,我们实际上是将.class文件丢给JVM去执行的,而每一个.class文件其实就是我们编写的一个类,我们在Java中使用一个类之前,JVM并不会在一开始就去加载它,而是在需要时才会去加载(优化)一般遇到以下情况时才会会加载类:

  • 访问类的静态变量,或者为静态变量赋值
  • new 创建类的实例(隐式加载)
  • 调用类的静态方法
  • 子类初始化时
  • 其他的情况会在讲到反射时介绍

所有被标记为静态的内容,会在类刚加载的时候就分配,而不是在对象创建的时候分配,所以说静态内容一定会在第一个对象初始化之前完成加载。

我们可以来测试一下:

java 复制代码
public class Person {
    String name = test();  //这里我们用test方法的返回值作为变量的初始值,便于观察
    int age;
    String sex;

    {
        System.out.println("我是普通代码块");
    }
    
    Person(){
        System.out.println("我是构造方法");
    }
    
    String test(){
        System.out.println("我是成员变量初始化");
        return "小明";
    }

    static String info = init();   //这里我们用init静态方法的返回值作为变量的初始值,便于观察

    static {
        System.out.println("我是静态代码块");
    }

    static String init(){
        System.out.println("我是静态变量初始化");
        return "test";
    }
}

现在我们在主方法中创建一个对象,观察这几步是怎么在执行的:

image-20220921000953525

可以看到,确实是静态内容在对象构造之前的就完成了初始化,实际上就是类初始化时完成的。

当然,如果我们直接访问类的静态变量:

java 复制代码
public static void main(String[] args) {
    System.out.println(Person.info);
}

那么此时同样会使得类初始化,进行加载:

image-20220921001222465

可以看到,在使用时,确实是先将静态内容初始化之后,才得到值的。当然,如果我们压根就没有去使用这个类,那么也不会被初始化了。

有关类与对象的基本内容,我们就全部讲解完毕了。


包和访问控制

通过前面的学习,我们知道该如何创建和使用类。

包声明和导入

包其实就是用来区分类位置的东西,也可以用来将我们的类进行分类(类似于C++中的namespace)随着我们的程序不断变大,可能会创建各种各样的类,他们可能会做不同的事情,那么这些类如果都放在一起的话,有点混乱,我们可以通过包的形式将这些类进行分类存放。

包的命名规则同样是英文和数字的组合,最好是一个域名的格式,比如我们经常访问的www.baidu.com,后面的baidu.com就是域名,我们的包就可以命名为com.baidu,当然,各位小伙伴现在还没有自己的域名,所以说我们随便起一个名称就可以了。其中的.就是用于分割的,对应多个文件夹,比如com.test

image-20220921120040350

我们可以将类放入到包中:

image-20220921115055000

我们之前都是直接创建的类,所以说没有包这个概念,但是现在,我们将类放到包中,就需要注意了:

java 复制代码
package com.test;   //在放入包中,需要在类的最上面添加package关键字来指明当前类所处的包

public class Main {   //将Main类放到com.test这个包中
    public static void main(String[] args) {

    }
}

这里又是一个新的关键字package,这个是用于指定当前类所处的包的,注意,所处的包和对应的目录是一一对应的。

不同的类可以放在不同的包下:

image-20220921120241184

当我们使用同一个包中的类时,直接使用即可(之前就是直接使用的,因为都直接在一个缺省的包中)而当我们需要使用其他包中的类时,需要先进行导入才可以:

java 复制代码
package com.test;

import com.test.entity.Person;   //使用import关键字导入其他包中的类

public class Main {
    public static void main(String[] args) {
        Person person = new Person();   //只有导入之后才可以使用,否则编译器不知道这个类从哪来的
    }
}

这里使用了import关键字导入我们需要使用的类,当然,只有在类不在同一个包下时才需要进行导入,如果一个包中有多个类,我们可以使用*表示导入这个包中全部的类:

java 复制代码
import com.test.entity.*;

实际上我们之前一直在使用的System类,也是在一个包中的:

java 复制代码
package java.lang;

import java.io.*;
import java.lang.reflect.Executable;
import java.lang.annotation.Annotation;
import java.security.AccessControlContext;
import java.util.Properties;
import java.util.PropertyPermission;
import java.util.StringTokenizer;
import java.util.Map;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.AllPermission;
import java.nio.channels.Channel;
import java.nio.channels.spi.SelectorProvider;
import sun.nio.ch.Interruptible;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
import sun.security.util.SecurityConstants;
import sun.reflect.annotation.AnnotationType;

import jdk.internal.util.StaticProperty;

/**
 * The <code>System</code> class contains several useful class fields
 * and methods. It cannot be instantiated.
 *
 * <p>Among the facilities provided by the <code>System</code> class
 * are standard input, standard output, and error output streams;
 * access to externally defined properties and environment
 * variables; a means of loading files and libraries; and a utility
 * method for quickly copying a portion of an array.
 *
 * @author  unascribed
 * @since   JDK1.0
 */
public final class System {
	  ...
}

可以看到它是属于java.lang这个包下的类,并且这个类也导入了很多其他包中的类在进行使用。那么,为什么我们在使用这个类时,没有导入呢?实际上Java中会默认导入java.lang这个包下的所有类,因此我们不需要手动指定。

IDEA非常智能,我们在使用项目中定义的类时,会自动帮我们将导入补全,所以说代码写起来非常高效。

注意,在不同包下的类,即使类名相同,也是不同的两个类:

java 复制代码
package com.test.entity;

public class String {    //我们在自己的包中也建一个名为String的类
}

当我们在使用时:

![image-20220921121404900](/Users/nagocoler/Library/Application Support/typora-user-images/image-20220921121404900.png)

由于默认导入了系统自带的String类,并且也导入了我们自己定义的String类,那么此时就出现了歧义,编译器不知道到底我们想用的是哪一个String类,所以说我们需要明确指定:

java 复制代码
public class Main {
    public static void main(java.lang.String[] args) {   //主方法的String参数是java.lang包下的,我们需要明确指定一下,只需要在类名前面添加包名就行了
				com.test.entity.String string = new com.test.entity.String();
    }
}

我们只需要在类名前面把完整的包名也给写上,就可以表示这个是哪一个包里的类了,当然,如果没有出现歧义,默认情况下包名是可以省略的,可写可不写。

可能各位小伙伴会发现一个问题,为什么对象的属性访问不了了?

image-20220921122514457

编译器说name属性在这个类中不是public,无法在外部进行访问,这是什么情况呢?这里我们就要介绍的到Java的访问权限控制了。

访问权限控制

实际上Java中是有访问权限控制的,就是我们个人的隐私的一样,我不允许别人随便来查看我们的隐私,只有我们自己同意的情况下,才能告诉别人我们的名字、年龄等隐私信息。

所以说Java中引入了访问权限控制(可见性),我们可以为成员变量、成员方法、静态变量、静态方法甚至是类指定访问权限,不同的访问权限,有着不同程度的访问限制:

  • private - 私有,标记为私有的内容无法被除当前类以外的任何位置访问。
  • 什么都不写 - 默认,默认情况下,只能被类本身和同包中的其他类访问。
  • protected - 受保护,标记为受保护的内容可以能被类本身和同包中的其他类访问,也可以被子类访问(子类我们会在下一章介绍)
  • public - 公共,标记为公共的内容,允许在任何地方被访问。

这四种访问权限,总结如下表:

当前类 同一个包下的类 不同包下的子类 不同包下的类
public
protected
默认
private

比如我们刚刚出现的情况,就是因为是默认的访问权限,所以说在当前包以外的其他包中无法访问,但是我们可以提升它的访问权限,来使得外部也可以访问:

java 复制代码
public class Person {
    public String name;   //在name变量前添加public关键字,将其可见性提升为公共等级
    int age;
    String sex;
}

这样我们就可以在外部正常使用这个属性了:

java 复制代码
public static void main(String[] args) {
    Person person = new Person();
    System.out.println(person.name);   //正常访问到成员变量
}

实际上如果各位小伙伴观察仔细的话,会发现我们创建出来的类自带的访问等级就是public

java 复制代码
package com.test.entity;

public class Person {   //class前面有public关键字

}

也就是说这个类实际上可以在任何地方使用,但是我们也可以将其修改为默认的访问等级:

java 复制代码
package com.test.entity;

class Person {    //去掉public变成默认等级
  
}

如果是默认等级的话,那么在外部同样是无法访问的:

image-20220921142724239

但是注意,我们创建的普通类不能是protected或是private权限,因为我们目前所使用的普通类要么就是只给当前的包内使用,要么就是给外面都用,如果是private谁都不能用,那这个类定义出来干嘛呢?

如果某个类中存在静态方法或是静态变量,那么我们可以通过静态导入的方式将其中的静态方法或是静态变量直接导入使用,但是同样需要有访问权限的情况下才可以:

java 复制代码
public class Person {
    String name;
    int age;
    String sex;
    
    public static void test(){
        System.out.println("我是静态方法!");
    }
}

我们来尝试一下静态导入:

java 复制代码
import static com.test.entity.Person.test;    //静态导入test方法

public class Main {
    public static void main(String[] args) {
        test();    //直接使用就可以,就像在这个类定义的方法一样
    }
}

至此,有关包相关的内容,我们就讲解到这里。


封装、继承和多态

封装、继承和多态是面向对象编程的三大特性。

封装,把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。

继承,从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。

多态,多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。

正是这三大特性,让我们的Java程序更加生动形象。

类的封装

封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要使用Getter和Setter方法来查看和设置变量。

我们可以将之前的类进行改进:

java 复制代码
public class Person {
    private String name;    //现在类的属性只能被自己直接访问
    private int age;
    private String sex;
  
  	public Person(String name, int age, String sex) {   //构造方法也要声明为公共,否则对象都构造不了
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    public String getName() {
        return name;    //想要知道这个对象的名字,必须通过getName()方法来获取,并且得到的只是名字值,外部无法修改
    }

    public String getSex() {
        return sex;
    }

    public int getAge() {
        return age;
    }
}

我们可以来试一下:

java 复制代码
public static void main(String[] args) {
    Person person = new Person("小明", 18, "男");
    System.out.println(person.getName());    //只能通过调用getName()方法来获取名字
}

也就是说,外部现在只能通过调用我定义的方法来获取成员属性,而我们可以在这个方法中进行一些额外的操作,比如小明可以修改名字,但是名字中不能包含"小"这个字:

java 复制代码
public void setName(String name) {
    if(name.contains("小")) return;
    this.name = name;
}

我们甚至还可以将构造方法改成私有的,需要通过我们的内部的方式来构造对象:

java 复制代码
public class Person {
    private String name;
    private int age;
    private String sex;

    private Person(){}   //不允许外部使用new关键字创建对象
    
    public static Person getInstance() {   //而是需要使用我们的独特方法来生成对象并返回
        return new Person();
    }
}

通过这种方式,我们可以实现单例模式:

java 复制代码
public class Test {
    private static Test instance;
    
    private Test(){}
    
    public static Test getInstance() {
        if(instance == null) 
            instance = new Test();
        return instance;
    }
}

单例模式就是全局只能使用这一个对象,不能创建更多的对象,我们就可以封装成这样,关于单例模式的详细介绍,还请各位小伙伴在《Java设计模式》视频教程中再进行学习。

封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现,要用什么由类自己来做,不需要外面来操作类内部的东西去完成,封装就是通过访问权限控制来实现的。

类的继承

前面我们介绍了类的封装,我们接着来看一个非常重要特性:继承。

在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有的成员。

比如说我们一开始使用的人类,那么实际上人类根据职业划分,所掌握的技能也会不同,比如画家会画画,歌手会唱,舞者会跳,Rapper会rap,运动员会篮球,我们可以将人类这个大类根据职业进一步地细分出来:

image-20220921150139125

实际上这些划分出来的类,本质上还是人类,也就是说人类具有的属性,这些划分出来的类同样具有,但是,这些划分出来的类同时也会拥有他们自己独特的技能。在Java中,我们可以创建一个类的子类来实现上面的这种效果:

java 复制代码
public class Person {   //先定义一个父类
    String name;
    int age;
    String sex;
}

接着我们可以创建各种各样的子类,想要继承一个类,我们只需要使用extends关键字即可:

java 复制代码
public class Worker extends Person{    //工人类
    
}
java 复制代码
public class Student extends Person{   //学生类

}

类的继承可以不断向下,但是同时只能继承一个类,同时,标记为final的类不允许被继承:

java 复制代码
public final class Person {  //class前面添加final关键字表示这个类已经是最终形态,不能继承
  
}

当一个类继承另一个类时,属性会被继承,可以直接访问父类中定义的属性,除非父类中将属性的访问权限修改为private,那么子类将无法访问(但是依然是继承了这个属性的):

java 复制代码
public class Student extends Person{
    public void study(){
        System.out.println("我的名字是 "+name+",我在学习!");   //可以直接访问父类中定义的name属性
    }
}

同样的,在父类中定义的方法同样会被子类继承:

java 复制代码
public class Person {
    String name;
    int age;
    String sex;

    public void hello(){
        System.out.println("我叫 "+name+",今年 "+age+" 岁了!");
    }
}

子类直接获得了此方法,当我们创建一个子类对象时就可以直接使用这个方法:

java 复制代码
public static void main(String[] args) {
    Student student = new Student();
    student.study();    //子类不仅有自己的独特技能
    student.hello();    //还继承了父类的全部技能
}

是不是感觉非常人性化,子类继承了父类的全部能力,同时还可以扩展自己的独特能力,就像一句话说的: 龙生龙凤生凤,老鼠儿子会打洞。

如果父类存在一个有参构造方法,子类必须在构造方法中调用:

java 复制代码
public class Person {
    protected String name;   //因为子类需要用这些属性,所以说我们就将这些变成protected,外部不允许访问
    protected int age;
    protected String sex;
    protected String profession;

  	//构造方法也改成protected,只能子类用
    protected Person(String name, int age, String sex, String profession) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.profession = profession;
    }

    public void hello(){
        System.out.println("["+profession+"] 我叫 "+name+",今年 "+age+" 岁了!");
    }
}

可以看到,此时两个子类都报错了:

image-20220921153512798

因为子类在构造时,不仅要初始化子类的属性,还需要初始化父类的属性,所以说在默认情况下,子类其实是调用了父类的构造方法的,只是在无参的情况下可以省略,但是现在父类构造方法需要参数,那么我们就需要手动指定了:

既然现在父类需要三个参数才能构造,那么子类需要按照同样的方式调用父类的构造方法:

java 复制代码
public class Student extends Person{
    public Student(String name, int age, String sex) {    //因为学生职业已经确定,所以说学生直接填写就可以了
        super(name, age, sex, "学生");   //使用super代表父类,父类的构造方法就是super()
    }

    public void study(){
        System.out.println("我的名字是 "+name+",我在学习!");
    }
}
java 复制代码
public class Worker extends Person{
    public Worker(String name, int age, String sex) {
        super(name, age, sex, "工人");    //父类构造调用必须在最前面
        System.out.println("工人构造成功!");    //注意,在调用父类构造方法之前,不允许执行任何代码,只能在之后执行
    }
}

我们在使用子类时,可以将其当做父类来使用:

java 复制代码
public static void main(String[] args) {
    Person person = new Student("小明", 18, "男");    //这里使用父类类型的变量,去引用一个子类对象(向上转型)
    person.hello();    //父类对象的引用相当于当做父类来使用,只能访问父类对象的内容
}

虽然我们这里使用的是父类类型引用的对象,但是这并不代表子类就彻底变成父类了,这里仅仅只是当做父类使用而已。

我们也可以使用强制类型转换,将一个被当做父类使用的子类对象,转换回子类:

java 复制代码
public static void main(String[] args) {
    Person person = new Student("小明", 18, "男");
    Student student = (Student) person;   //使用强制类型转换(向下转型)
    student.study();
}

但是注意,这种方式只适用于这个对象本身就是对应的子类才可以,如果本身都不是这个子类,或者说就是父类,那么会出现问题:

java 复制代码
public static void main(String[] args) {
    Person person = new Worker("小明", 18, "男");   //实际创建的是Work类型的对象
    Student student = (Student) person;
    student.study();
}
image-20220921160309835

此时直接出现了类型转换异常,因为本身不是这个类型,强转也没用。

那么如果我们想要判断一下某个变量所引用的对象到底是什么类,那么该怎么办呢?

java 复制代码
public static void main(String[] args) {
    Person person = new Student("小明", 18, "男");
    if(person instanceof Student) {   //我们可以使用instanceof关键字来对类型进行判断
        System.out.println("对象是 Student 类型的");
    }
    if(person instanceof Person) {
        System.out.println("对象是 Person 类型的");
    }
}

如果变量所引用的对象是对应类型或是对应类型的子类,那么instanceof都会返回true,否则返回false

最后我们需要来特别说明一下,子类是可以定义和父类同名的属性的:

java 复制代码
public class Worker extends Person{
    protected String name;   //子类中同样可以定义name属性
    
    public Worker(String name, int age, String sex) {
        super(name, age, sex, "工人");
    }
}

此时父类的name属性和子类的name属性是同时存在的,那么当我们在子类中直接使用时:

java 复制代码
public void work(){
    System.out.println("我是 "+name+",我在工作!");   //这里的name,依然是作用域最近的哪一个,也就是在当前子类中定义的name属性,而不是父类的name属性
}

所以说,我们在使用时,实际上这里得到的结果为null

image-20220921160742714

那么,在子类存在同名变量的情况下,怎么去访问父类的呢?我们同样可以使用super关键字来表示父类:

java 复制代码
public void work(){
    System.out.println("我是 "+super.name+",我在工作!");   //这里使用super.name来表示需要的是父类的name变量
}

这样得到的结果就不一样了:

image-20220921160851193

但是注意,没有super.super这种用法,也就是说如果存在多级继承的话,那么最多只能通过这种方法访问到父类的属性(包括继承下来的属性)

正在加载页面,请稍后...