上帝视角学JAVA- 基础07-类05【2021-08-06】

1、static 关键字

static 翻译为中文就静态的

为什么有静态的这个东西出来呢?

考虑一下需求:

以前我们讲过,类中的属性和方法是模板,是属于类的对象的东西。但是有些东西,我希望是每一个对象都有,而且都是一样的。不能被继承和重写。那么为了满足这个需求,这些东西就必须是属于类的,而不再属于类实例化的对象。因为属于对象可以被继承。而属于这个类,不能被继承。而且类就是一个模板,既然是属于模板,那么每一条他实例的对象都可以访问。

由此,static关键字就出来了,static关键字就是 划分了属性或者方法的归属权,如果加上了static,归属权将属于类,而不再属于类实例化的对象。内部是如何实现的呢?在内存中,如果是属于对象,那么这些东西就会被保存在对象所在的堆空间。

而属于类的话,将不再保存到对象里面,而是单独一份保存。这样,继承不会拷贝这些东西,即无法被继承。单独保存的堆空间的地址就是类名。又由于对象是类实例化的,所有对象是可以访问这块内存,但是并不属于对象。

  • 加上static 关键字,属性或者方法归属性变成了 类

  • 归属权都变了,那么加上了static的变量叫做类变量,普通的还是叫成员变量。同理有类方法、成员方法的区分。

  • 类只会加载一次,类的东西也只会被保存一份。

  • static 关键字的东西不能被继承

  • static 关键字定义的东西,不属于对象,对象可以访问。【就像租房子,房子可以用,但不代表房子就是你的】

  • static 关键字定义的东西可以通过类名找到,调用。【主人当然可以进行访问、修改】

    但是类名不能调用对象的方法,对象可以调用类的方法、属性是因为类方法、属性随类加载而加载,而且只有一份。但是类名调用对象的属性和方法是有问题的,因为对象有很多,通过类名不知道调用哪一个对象的,有歧义。而且对象不一定创建了,即调用不存在的也会导致报错。所有类名点的方法不能调用类的成员属性、方法。只能调用自己的类属性、类方法。

  • static 可以修饰 属性、方法、代码块、内部类;不能修饰构造器!

    不能修饰构造器的原因是构造器是用来实例化具体对象的,归属于类了,就不能实例化不同对象了。

// 年龄
public int age;
// 性别
public String sex;
// 静态属性: 体重   
public static int weight = 10;
// 静态方法: testStatic
public static void testStatic(){
    weight = 20;
}

如上,静态方法 testStatic 可以访问静态的属性 weight。但是静态方法不能访问 非静态属性 age和sex。

这样写idea就报错了,编译都同不过,含义为 静态方法中不能使用非静态属性。

原因是什么?

原因就是类和对象在内存中存储的位置以及出现的时间不同。属于类的动向如属性和方法会在类一加载就在内存中出现。就可以用类名点的方法进行调用。而对象必须要实例化即new出来才能在内存中出现。即类东西比对象出现的早,类东西一定会出现,而对象不一定会出现。普通的属性是属于对象的,如果对象都不存在,静态方法一开始就有了,在里面调用一个可能不存在的属性,就会出错!

基于这个原因,可以推理出,静态的只能调用静态的

非静态的由于出现时间比较晚,所以是可以调用一定存在的静态的属性或方法。即非静态方法的可以调用静态和非静态的属性和方法。

看一下内存模型:

栈:放局部变量,比如类名,对象名,

堆:放new处理的东西:对象

方法区:放类的加载信息、静态域、常量池。

静态域:放 加了static 关键字的东西。

可以看到:对象和静态域根本就是住在2个区域的

1.1 static 什么时候需要用?

首先分析static干了什么,加上static之后,属性、方法、内部类、代码块就变成了类的,归属权变更。可以说与对象没有什么关系了。可以推出,当不需要对象的时候,就可以使用static。

再看看 别人写的:

我们前面将到的一个工具类 Arrays:

这个Arrays 里面的方法,可以看到基本都是 static。加上了static 就说明不需要对象,直接通过类名点的方法就可以使用。

不需要对象也可以理解为这个类的任意对象都具有这个特点,具备公共性。

可以得出:

1、当我们需要某个方法不通过对象,直接就可以类名点的方式使用时,就可以声明未static,工具类用的最多。

2、声明未static其实就是每一个对象公有的,都可以访问。当某个属性是所有对象都有的,就可以提取出来,声明为static。

2、单例设计模式

其实前面讲 多态时,已经讲过一种设计模型。替换if else那一部分内容。

这里开始讲 单例设计模式。

单例的意思是:如果某个类设计为单例的,那么内存中这个类只能存在一个对象。每次去获取,修改等操作都是对这一个对象进行。

关键就在与如果实现,只实例化1个对象。

分析:

类实例化对象一定要调用构造方法的,所以一定要对构造方法动刀。

实例对象new 一下 构造方法,每new 一下就会产生1个新的对象。因此为了保证只有1个对象,new 只能使用一次。所以要限定构造方法的权限,我们自己来控制,外界不能new。故构造方法必须是私有的,如果是public,外界可以访问,就可以多次调用,不能达到只创建1个对象的目的。

结论:构造方法必须是私有的

当 构造方法为私有,意味着外界不能以new 的方式进行创建对象。所以类的内部必须自己调用。要提供一个公共的方法给外界调用,我们在这个方法里面加上逻辑判断,如果存在就不把已存在的对象给外界,如果没有对象,就new一个给外界。这样来保证只有1个对象。

又由于外界不能使用new创建对象,不能通过对象点的方法调用我们提供的公共方法。只剩下一条路,就是static 方法。这样就不需要对象,也可以调用我们提供的这个方法。

结论:提供一个静态的公有方法,在这个方法里面实现对象的创建逻辑。返回给外界一个对象。

由于我们需要判断对象是否已经存在,所以需要一个变量来存储这个对象。不然就找不到这个对象了。这个存储对象的变量 的类型就是本类。而且是在静态方法里面进行判断的,意味着要被静态方法访问,这个变量就必须声明为静态的。这个变量存储了对象,声明了static,外界是可以通过类名点的方式获取,但这是不对的,如果外界直接获取,我们就没法控制对象只有1个,因为外界直接获取这个类属性可能是空的。而且没有走我们通过的静态公共方法,也不能保证是唯一的。所以这个属性必须是 私有权限。即外界不能通过类名点的方式访问。

结论:必须有一个私有的、静态的、类类型的 变量存储 对象。

来看下面这个例子:

// 单例实现方式1:饿汉式  
public class SingletonMode1 {
​
    // 私有的、静态的 SingletonMode1 类型的 变量 存储 对象
    private static SingletonMode1 instance = new SingletonMode1();
​
    // 公有的、静态的 返回值为 SingletonMode1 类型的 方法 给外界提供对象
    public static SingletonMode1 getInstance(){
        return instance;
    }
    // 私有的构造方法 禁止外界创建对象
    private SingletonMode1(){
​
    }
}
 
@Test
public void test1(){
    // 外界 获取对象
    SingletonMode1 instance = SingletonMode1.getInstance();
}

上面是单例的实现方式1,饿汉式。饿汉式只是一个名字。是根据 这种方法的特点叫的。特点是 存储类的变量 instance 在类一开始加载时,就会给赋予上初值,调用构造函数。就像个饿汉一样,上来就干。

再看下面这种方式:

// 单例实现方式1:懒2汉式
public class SingletonMode2 {
    
    // 私有的、静态的 SingletonMode2 类型的 变量 存储 对象, 默认初始化。
    private static SingletonMode2 instance;
​
    // 公有的、静态的 返回值为 SingletonMode2 类型的 方法 给外界提供对象
    public static SingletonMode2 getInstance() {
        // 如果 instance 是null 就创建对象,并赋值给instance,如果不是null,就返回给外界
        if (Objects.isNull(instance)) {
            instance = new SingletonMode2();
        }
        return  instance;
    }
    
    private SingletonMode2(){
        
    }
}
​
@Test
public void test1(){
    SingletonMode2 instance = SingletonMode2.getInstance();
}

上面这个方法是另一种实现,并没有类加载时就调用构造方法,而是当外界需要对象时,才去判断一下要不要调用。这就像一个懒汉一样,需要才干活。

这种方法也可以实现单例模式,但是具有线程不安全。因为当 多个人同时执行获取实例时,需要先判断,如果某个线程创建对象的过程中,又有一个线程判断来判断是否有对象,由于对象没有创建完成,所有会得到没有对象的结果。然后此时第一个线程又创建好了对象,第二个线程会接着又去创建对象。因为刚刚判断成功了。

产生这种结果的原因是 判断是否有对象与创建对象不是原子性的。

即:判断是否有对象的时候,可以去创建对象。创建对象的时候也可以去判断是否有对象。如果是原子性的,就是判断是否有对象的时候,不能去创建对象,判断完了之后才可以继续执行创建对象。同理,必须执行完创建对象的逻辑,才能去判断是否有对象。

即并行操作必须是串行,而且必须都完成,或者都失败,才能继续执行后续代码。

所谓原子性,是物理学上认为 原子是不可分割的。我们这里说的原子性是指 某几个操作是不可分割操作,必须同时成功,同时失败,虽然是多个操作,但是要像一个操作这样。 实现原子性 很自然的想法就是打包,把这些操作变成一个整体,对外服务。实现打包就是加锁!

比如日常操作,将几个文件打包成一个压缩文件,你要是删除了这个压缩文件,那么里面的所有文件都被删除了,给压缩文件添加密码之后,里面的所有文件都需要密码才能访问。即将多个文件像一个文件一样处理了。

加锁就可以实现类似的操作。

第一种实现方法是天生线程安全的,因为类只会加载一次,类属性会随着类加载而加载。所以也只会加载一次。对象在内存中只会有一份。

第二种单例的实现,变成线程安全的修改,将在后续多线程内容中讲解!

2.1 什么时候用单例

需要从单例的特点分析:内存中只需要1个对象,即这个对象的内容是共享的。

实际例子:

  • 计数器: 单例模式的计数器就不会出现,不同的对象计数不同,不能累加。

  • 日志:日志一般是需要往文件内容追加的,对个对象就会有多个文件。不好追加内容。

  • 数据库连接池

  • 读取配置文件的类

  • windows中的 任务管理器

  • windows中的 回收站

  • 存储全局的某个状态

3、main方法

public static void main(String[] args) {
​
}

main 方法是程序的入口。

为什么呢? 简单的很,首先JAVA程序是运行在JVM里面的,JVM运行java代码需要找一个口子开始执行。这个口子就是main方法。已经在JVM里面写死了,这个方法名就是main。

修饰符public 代表公共权限:这是因为JVM需要调用 main方法,得要有权限。

static 表示这是类方法:因为JVM 不需要创建对象就可以使用这个方法。

void 表示 返回值为空:JVM 执行方法,要这返回值干啥?

参数列表 String[] args :

对于参数args是一个 String 类型的数组。这是为了兼容1.5之前的写法。现在其实可以用 String... args 代替。

public static void main(String... args) {
    System.out.println(1111);
}

这说明,main方法是可以接收参数的,可以接收多个String 类型的参数。在命令行时,是可以传入参数给main方法的,idea也是可以设置给main方法传递参数的。

仔细一看,main方法与普通静态方法没有什么区别。只不过JVM可以调用就是了。

4、代码块

代码块 也是属于类的成员。写在类里面。用一对大括号包起来。

public class Animal {
​
    public String type = "Animal";
    public int age;
    // 代码块
    {
        // 输出语句
        System.out.println(this.age);
        // 属性初始化
        age = 10;
    }
    
    // 静态属性: 体重
    public static int weight = 10;
    // 静态代码块
    static {
        // 输出语句
        System.out.println(weight);
        // 属性初始化
        weight = 20;
    }

看上面的代码:有2个代码块,第一个是非静态,第二个是静态的,加了static修饰。

注意,代码块只能被static修饰,不能被权限修饰符修饰。

  • 代码块里面可以有 输出语句。非静态代码会随着对象的创建而加载并执行。

  • 代码块加了static 依然是归属权发生变化,变成类的代码块。随着类的加载而加载,并且会自动执行。

代码块出现的原因是什么?

代码块一般是用来做属性的初始化,但是如果属性的初始化比较复杂,直接初始化就不适用了。这些初始化逻辑就可以写在代码块里面。

/**
 * JDBC 工具类
 */
public class JDBCUtils {
    private static String url;
    private static String user;
    private static String password;
    private  static String driver;
​
    /**
     * 文件读取,只会执行一次,使用静态代码块
     */
    static {
        //读取文件,获取值
        try {
            //1.创建Properties集合类
            Properties pro = new Properties();
            //获取src路径下的文件--->ClassLoader类加载器
            ClassLoader classLoader = JDBCUtils.class.getClassLoader();
            URL resource = classLoader.getResource("jdbc.properties");;
            String path = resource.getPath();
            //2.加载文件
            pro.load(new FileReader(path));
            //3获取数据
            url = pro.getProperty("url");
            user = pro.getProperty("user");
            password = pro.getProperty("password");
            driver = pro.getProperty("driver");
            //4.注册驱动
            Class.forName(driver);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    /**
     * 获取连接
     * @return 连接对象
     */
    public static Connection getConnection() throws SQLException {
        Connection conn = DriverManager.getConnection(url, user, password);
        return conn;
    }
​
    /**
     * 释放资源
     * @param rs
     * @param st
     * @param conn
     */
    public static void close(ResultSet rs, Statement st,Connection conn){
        if (rs != null){
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(st != null){
            try {
                st.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (conn != null){
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

看上面这个类,静态代码块干了写啥。里面居然还有try catch捕获异常。异常后面会讲。这里的逻辑是读取配置文件,然后为4个私有属性赋值。还是干的属性初始化的活。但是这个活比较复杂,不能直接用赋值语句进行初始化。就用上了代码块。

这里用的是静态代码块,初始化的是静态变量。静态变量是属于类的,如果把这些复杂的逻辑放到构造器中进行是不合适的,因为每一次new对象都会调用构造器。属性就不是只初始化一次了。想要实现复杂的初始化静态变量,还要求只能初始化一次,只能用静态代码块(最近选择)。这是静态代码块最大的作用!

至于非静态代码块,可以把这些逻辑放到构造器中进行。因为非静态属性即成员属性是属于每一个类的。类构造器进行复杂初始化是可以的。所以,用代码块,只有静态代码块具有无可替代的作用!

代码块可以写多个吗?

可以,代码块的个数没有限制。不过一般不会这么无聊写多个,因为写多个和写1个效果是1样的。如果定义了多个,按照顺序执行。

懒的人是不会定义多个的,如果不是需要复杂初始化,懒的人一个也不想定义!

5、属性赋值顺序补充

前面讲到 属性是

1、默认初始化

2、显示初始化

3、构造器初始化

4、有对象后,对象点进行初始化

现在增加了一个 代码块中初始化。

代码块初始化如果是静态的,随类加载而运行,一定在构造器之前。因为构造器都没调

如果是非静态代码块,是调用构造器是进行对象实例化。代码块会在构造器逻辑执行之前执行。

然后是代码块与 显示初始化谁先谁后? 测试的答案是 平级。看书写先后顺序

所有最终的顺序是:

1、默认初始化
2、显示初始化\代码块初始化
3、构造器初始化
​
4、有对象后,对象点进行初始化
    
前3条都是类里面初始化,第4条是类外面。

思考一下?其实代码块是为了解决显示初始化不能进行复杂逻辑的初始化为题而出现的。最准确的需求是:实现:使用复杂逻辑只初始化静态变量一次

有点像是替换显示初始化而生的。因此他们的 顺序一样,看书写先后。

6、final 关键字

public final int a = 5;
public static final int b = 5;

final的含义是 最终的,最终的也意味着不可变了。

它的出现是为了 定义常量,或者说定义 不准别人修改的 变量。(不能修改的变量等价于常量)

什么时候会需要用这个呢?

比如说某个方法,定义为final,那么子类将只能继承而不能重写。因为是final最终的,你要是能改,最终的就没有意义。

这就像老爹传给你的东西,只准你用,不准你改!

  • 修饰 变量 就等价于定义了常量

类中的final 变量不能只是默认初始化,还必须使用类内的其他初始化如 直接使用显示初始化、代码块初始化、构造器初始化。

  • 修饰方法 子类继承将不能重写这个方法

  • 修饰 类 ,这个类不能被其他类继承。即丁克(太监)了,不能有儿子。

public final class String

String 类就是一个 final 类,禁止被继承。

原因是JAVA设计者不希望使用者扩充功能,或者说重写里面的方法。即这个类已经是最好的了,其他人只能用,不能改。

总结:final 修饰的东西,只能用,不能改!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值