重拾JavaSE基础——类与对象、封装和继承

最近一直都在写业务代码,想了一下自己好像没有真的系统的学习过Java,决定重头来过,抽半个月时间重新学习

目录

  1. 我们应该用人类的思维方式学习Java
  2. 类与对象

    1. 类的结构
    2. 创建一个对象的方式
  3. this 关键字
  4. static 关键字

    1. 静态成员变量和方法被存放在哪里
    2. 如何访问静态成员变量
  5. 封装

    1. 三大特性怎么来的
    2. 封装的好处
    3. 如何封装
  6. 继承

    1. 关于继承的一些说法
    2. super关键字
    3. 方法重写

      1. 重写标志@Override
      2. 好好的为什么要重写方法
      3. 重写方法需要注意
    4. 子类在堆中的结构
    5. 子类构造器

      1. 构造器的相互调用
    6. 继承的特点
  7. 引用类型
  8. 总结

我们应该用人类的思维方式学习Java

以前学习Java总是会想,用C语言跟Java编程到底有什么区别呢,前者是面向过程的语言,后者是面向对象的语言,大家最后在CPU面前都是一行一行的执行代码,凭什么Java是面向对象的呢?

经过一段漫长的时间,我才慢慢意识到,面向过程和面向对象是一种思维方式,Java是根据人类的思维方式进行编程,而C语言是根据事情的解决方式进行编程。回想我一开始写的代码,都是一个main方法处理所有的事情,这是面向过程的思维,这也是我看不懂源码的原因之一,所以在Java世界中,我们是造物主,编程就是给一类事物赋予生命的迹象

类与对象

  • :就是相同事物的共同特征的抽象,说白了就和你心里想的类别是一个意思,例如人类(我们人体基本结构相同)、解热镇痛类药物(吃了都可以解热镇痛)
  • 对象:就是具体存在的实例,说白了就是某一个类别下的具体的某样东西,如上面说到的解热镇痛药物中的布洛芬,
其实用人类世界思维去理解就可以了

类的结构

一个类中包含以下五个部分,可以少一两个:

  • 成员变量
  • 构造器
  • 成员方法
  • 代码块
  • 内部类

值得一提的是:

  1. 千万别把构造器叫成构造函数,Java中没有函数的概念
  2. 构造器是用于初始化对象的,不是创建对象的

创建一个对象的方式

  • 最常规的方法:类名 对象名称 = new 构造器
  • 反射,ORM框架如MyBatis中大量使用
  • ...

this 关键字

说到这个我就想起JavaScript里面的this,当时刚开始学编程,一直觉得他很玄乎。this其实就表示当前对象,具体有以下使用场景:

  • 在构造器中,如

    public UserDTO(String username, String password) {
        this.username = username;
        this.password = password
    }
    public static void main(String[] args) {
        UserDTO userDTO = new UserDTO("Rhythm", "1024");
    }

    这里的this就表示将要被初始化的对象,也就是userDTO所引用的对象

  • 在实例成员方法中,如

    public void setUsername(String username) {
        this.username = username;
    }
    public static void main(String[] args) {
        UserDTO userDTO = new UserDTO("Rhythm", "1024");
        userDTO.setUsername("Rhythm2019")
    }

    谁调用了这个方法,this就指代谁,这里就是指代.前面的userDTO

  • 在synchronized中,如

    public class Main {
        public static void main(String[] args) {
            MyConnectionPool myConnectionPool = new MyConnectionPool();
            MyThread t1 = new MyThread(myConnectionPool);
            MyThread t2 = new MyThread(myConnectionPool);
        }
    }
    class MyThread extends Thread {
        
        private MyConnectionPool myConnectionPool;
        
        public MyThread(MyConnectionPool myConnectionPool) {
            this.myConnectionPool = myConnectionPool;
        }
        
        public void run() {
            myConnectionPool.getConnection();
            System.out.println("获取到新连接!")
        }
    }
    class MyConnectionPool {
        
        private static final Integer POOL_DEFAULT_SIZE = 20;
        
        private static List<Connection> connectionList;
        
        private static List<Integer> avaikList;
        
        static {
            url = "...";
            username = "...";
            password = "...";
            connectionList = new ArrayList<>(POOL_DEFAULT_SIZE);
            avaikList = new ArrayList<>(POOL_DEFAULT_SIZE)
            
               try {
                Class.forName("com.mysql.jdbc.Driver");
                
                for (int i = 0; i < POOL_DEFAULT_SIZE; i++) {
                    Connection conn = DriverManager.getConnection(url, username, password);
                    connectionList.add(conn);
                }
            } catch {
                e.printStackTrace();
            }
           
        }
        
        public Connection getConnection() {
            synchronized (this) {
                for(int i = 0; i < POOL_DEFAULT_SIZE; i++) {
                    if (availList.get(i) == 0) {
                        connectionList.get(i);
                        availList.set(i, 1);
                    }
                }
            }
        }
    }
手写的手写的没去运行,只是为了演示一下这个场景,这是我以前写的一个小demo,大家知道这个意思就好。

这里的this其实就是myConnectionPoolsynchronized (this)中的this相当于一把钥匙,跑得快的线程拿着钥匙先进入代码块,还把门给锁了,等他好了出来把钥匙递给第二个线程,第二个线程才能进入代码块。仔细想,从头到尾MyConnectionPool对象只new了一次,所以只有一把钥匙,所以两线程拿Connection的时候不会拿到同一个

  • 作为方法:this(),这个等等和super()一起总结把

static 关键字

相信大家也用的很多,可以将类中的成员变量和方法变为静态,也可以声明代码块使类加载时时候运行。这里要注意,加上static的成员变量和方法属于类,不属于对象

静态成员变量和方法被存放在哪里

看一下先面对代码,我们用图来说明一下吧

public class Main {

    public static void main(String[] args) {
        Test t1 = new Test();
        Test t2 = new Test();
        t1.add();
        t2.add();
        System.out.println(Test.i);
    }
}

class Test {
    
    public static Integer i = 0;
    private Integer k;
    public void add() {
        i++;
    }
}

载入字节码文件到方法区,大家注意左边的代码运行情况哈,忘记标红了
静态变量存储1.png
堆中创建Class对象,方法区中保存引用(地址),这时也会在方法区中的静态区初始化静态成员变量。如果静态成员变量是另一个对象,JVM马上加载新类的字节码,并创建新对象,把新对象的堆地址保存到自己的静态区中

静态变量存储2.png

接着在堆中创建两个对象,对象中保存实例成员变量的值,并将方法区中的成员方法字节码的存放地址保存到对象里。堆中的对象创建完成后会把引用返回到栈中,给正在执行的main方法使用,

静态变量存储3.png

最后执行add方法,静态区中的i的值发生了变化。如果是多线程同时进行操作就会出现问题

静态变量存储14.png

总结一下:

  1. 堆是线程共享的,注意线程安全问题
  2. 堆中的对象只保存成员变量的值,不保存成员方法,只保存地址
  3. 如果静态成员变量是对象类型的话,静态区保存的是引用(地址)
  4. 如果我的理解有错误麻烦评论区告诉我哈

如何访问静态成员变量

  • 在在本类中的静态/实例方法中直接访问,如isBlank()
  • 加上类名访问,如StringUtils.isBlank();
  • 通过对象访问(不推荐)

    StringUtils stringUtils = new StringUtils();
    stringUtils.isBlank*();

封装

说到封装,其实就是成员变量私有,对外提供一套getset方法,是Java中的一大特性

三大特性怎么来的:

Java为什么有继承、封装和多态三大特性,这其实这写只是一种风格,举个例子吧,我刚进大学军训的时候,发现周围的同学都来自不同省份,有四川的山西的等等等等,很快我就结识个好兄弟,是山西太原的,长得很帅,军训的时候师姐们都围着她找他要微信。过了一段时间我与班上的其他同学也有了接触,我们班有个小姐姐也是山西的,长得挺好看的,这时候我感悟到,山西的人儿都长得很帅很好看的呀。

所以Java为什么有这三个特性,其实也是这样的。人们刚开始用Java写代码其实也没这些特性,这时刚好有一些人把对象封装了起来,于是就有人发现这样做有好处(等等说)。愈来愈多人开始模仿,到我刚学Java的时候,咦大家怎么都这样写呀,Java的对象是不是一定要这样封装呀,最后大家都认为Java的对象需要封装,封装成为了三大特性之一。

封装的好处

  • 安全 :即使private修饰,我们仍然可以用反射的手段访问该字段的,哪里安全了呀?后来受人指点,安全只是相对的。封装的安全主要体现在对一些数据进行隐藏,对外暴露一套getset方法,所有的类要访问这个类的成员变量只能调用这一套getset方法。

    其实跟我们人类现实生活是一样的呀,就是为了合理隐藏,合理暴露,类似于我们穿衣服,也是要把眼睛鼻子露到外面呼吸的吧

  • 可以实现组件化: 例如将哆啦A梦的口袋看作一个类,里面的小道具是成员变量,当你跟他说你要小道具(太久没看了想不起来那个能在空中飞的那个叫什么了),你无需关注这个道具到底放在口袋里面的哪个位置,道具是怎么诞生的,哆啦A梦给你你拿去用就行了。

    这种方式就类似于工厂模式,不需要在意这个类里面的构造,你只要知道调用一套getset方法就可以了。

  • 存入取出值的时候可以对数据进行处理 :这个也好理解,一个UserDTO实体类,里面有一个字段status表示该账户是否可用,在数据库中1表示可用2表示冻结,那返回前端的时候可以对数字进行转换,将他转换成字符串“可用”或者“冻结”,前端不需要判断直接可以显示啦。这里只是举个例子哈,具体怎么返回还是得看实际情况。

如何封装

  • 成员变量加上private关键字
  • 对外提供getset方法

具体来说可以用Intellij IDEA中的Alt+Insert,或者用lombok@Data注解

继承

继承也是Java的特性之一,继承的目的自然是方便代码复用啦,这里就不举例子了,值得一提的是,Java的继承不像我们人类世界的继承,我们人类社会中老爸有本事儿子不一定会有本事,当然啦我也想像我爸一样有本事。但是在Java中子类是比父类强的。

这里有一句话大家要记在心里:子类可以继承父类的所有实例成员变量和实例方法,子类是父类的增强

关于继承的一些说法

  1. 父类的构造器是不能被继承的,看完下面就知道为什么了
  2. 父类中带有private修饰符的成员或方法是可以被继承的,只是不能被直接访问,这个我想起了我之前写的一些实体类

    public class BaseEntity {
    
        private String id;
        private Date gmtCreate;
        private Date gmtModified;
    }

    因为每一个实体类都有id创建时间修改时间,所以我把它做成了父类,让其他实体类继承

    public class BaseUserInfo extends BaseEntity implements Serializable {
    
        private String accountId;
        private String avatarUrl;
        private String userName;
        private String password;
        private Integer status;
        private String phoneNum;
        private String sex;
        private Date birthday;
    }

    你看,虽然父类的字段是私有的,但是ORM框架还是可以通过反射的形式给我们的实体类赋值

  3. static`修饰的成员变量和方法是不嫩被继承的,这个也是没有必要的,等下看个图也能明白

    public class Main {
    
        public static void main(String[] args) {
            Children children = new Children("Rhythm", "666666");
            System.out.println(children.i); 
        }
    }
    
    class Parent {
    
        public static Integer i = 2;
    
        public String username;
        public String password;
        public Parent() {
        
        }
    
        public Parent(String username, String password) {
    
        }
    }
    
    class Children extends Parent {
    
        public Children(String username, String password) {
            this.username = username;
            this.password = password;
        }
    }

    还是可以看到输出结果的,也就是说属于对象的东西才能体现继承

    2
所以继承并非覆盖,在我看来,如果我理解错误欢迎大家指出

super 关键字

与this差不多,super表示当前对象的父类引用,这里就不再赘述了

方法重写

相信大家用的还是挺多的,不过一定要注意,怎么样才能算是对方法进行了重写

重写标志@Override

从前有些程序员想重写父类的方法的时候,不小心把方法名给写错了,他以为自己重写了父类的方法其实并没有。为了杜绝这个情况,Java规定你要重写方法的话要加上@Override注解,这样我编译的时候帮你检查一下你的方法名能不能跟父类的匹配得上。所以子类方法加上@Override才算得上重写

总结一下@Override的作用

  1. 增加可读性
  2. 编译器会帮你检查是不是真的重写了方法

好好的为什么要重写父类的方法

  • 因为你觉得父类的方法写的不够好
  • 因为你要对父类的方法进行加强

重写方法需要注意 (看一下就行)

  • 方法名和形参列表与父类一致
  • 子类的返回类型的范围要小于等于父类(比父类更具体)
  • 子类的方法修饰符要大于等于父类
  • 子类抛出的异常要小于等于父类
总之:重写的时候跟父类写的一样就得了,遵循声明不变,重新实现的原则就好

还有两点需要注意一下

  1. 私有方法不能被重写
  2. 静态方法不能被重写,静态方法都是类名调用的,所以你要调用父类就必须写父类的

子类在堆中的结构

继承了父类的子类对象在堆中其实存在两块区域,一个是this区,保存子类的成员变量,一个是super区,保存父类的成员变量。

这里有一段代码,Parent是个父类,有两个成员变量username和·password,Children类继承Parent,Children类自己有一个test`方法

Children c1 = new Children();
Children c2 = new Children();
c1.username;
c2.text();

当执行到c1.username的时候,JVM会先去this区找,找不到username这个成员变量,于是再去super区中找,最后返回结果。而对于方法testJVMthis区可以找到就不会再去super区找了

父类与子类.png

注意:

  1. 看上去堆被分成两个区,但是对外只有一个引用
  2. 不同的类的静态区的内容是互不干扰的

子类构造器

这里也是有说法的,看看下面的代码

public class Main {

    public static void main(String[] args) {
        Children children = new Children();
    }
}

class Parent {

    public Parent() {
        System.out.println("父类构造器");
    }
}

class Children extends Parent {

    public Children() {
        System.out.println("子类构造器");
    }
}

结果:

父类构造器
子类构造器

其实就是在初始化子类的时候会顺便会初始化父类,也就是说上面的代码中子类的构造器的第一行隐藏了一句super(),大家回忆子类对象在堆中的结构,是不是子类中也包含了父类,那父类也需要初始化一下的呀

构造器的相互调用

  • 没有指定的话,子类默认调用父类的默认构造函数
  • 子类构造器可以调用子类其他的构造器或父类其他构造器,子类调用兄弟构造器用this(...),子类调用父类其他构造器使用super(...),不写就调用默认的构造器
  • this(...)super(...)一定在构造器的第一行,二者只能存在一个
大家也不用担心如果子类调用了兄弟构造器父类会不会不能被初始化,别忘了兄弟构造器的第一行也会调用父类构造器

总之,大家看代码的时候一定要记住子类构造器的第一行一定是在调用自己其他的构造器或者是父类的构造器,如果没有写,默认执行super()

继承的特点

  • 单继承:如果被面试官问道这个可以用反证法来说明,如果可以多继承,以下代码将会出现二义性

    class A {
        public void test() {
            System.out.println("A");
        }
    }
    class B{
        public void test() {
            System.out.println("B");
        }
    }
    class C extends A,B {
        public static void main(String[] args) {
            C c = new C();
            c.test(); //Java就懵逼了
        }
    }
  • 多层继承,就像家谱图一样,在Java中老祖宗Object是最弱的。
  • 可以有多个子类

引用类型

Java中数据类型分为引用类型基本类型,基本类型即使布尔类型浮点型那些,引用类型其实就是对象的意思,对象可以作为参数和返回值,被各种方法传递,但是我们要明确,传来传去最后传的都只是一个引用(地址),中途方法对对象的成员变量进行了修改,那肯定是会影响到其他方法的运行结果的

写在最后

写了一天,收获还是很大的,听了一些公开课感觉基础很重要,所以感谢现在还在努力的自己,加油!

阅读 133

推荐阅读