美文网首页
Java基础系列之面向对象

Java基础系列之面向对象

作者: 6bc9f71c8f0c | 来源:发表于2018-11-14 16:28 被阅读0次

    在初遇章节我们就谈到过Java是一门面向对象的语言,那么什么是面向对象呢?既然有面向对象语言,是否就有其他的语言?面向对象又能给我么带来什么好处呢?接下来,我们将在这个章节探讨下面向对象。

    面向过程和面向对象


    在目前的软件开发领域有两种主流的开发方法:结构化开发方法(面向过程)和面向对象开发方法。早期的编程语言C、Basic、Pascal等都是结构化编程语言,随着时代的变迁,软件的发展,人们发现了一种更好的可复用、可扩展和可维护的的方法,即面向对象,代表语言有C++,C#,Ruby,Java等。

    • 面向过程
      主张按功能来设计程序,特点是:自上而下,逐步求精,模块化等。结构化程序设计的最小单元是函数,每个函数都负责完成一个功能。局限性有两点:一,设计不够直观,与人类习惯思维不一致;二,适应性差,可扩展性不强。
    • 面向对象
      更优秀的程序设计思想,基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。最小单位是类,由类可以生成系统中多个对象。

    面向对象的基本特征


    • 封装
      隐藏细节,通过公共方法暴露出该对象的功能。比如说一台电脑,我们在不拆机的情况下看不到里面的主板,cpu,内存条,这些好比是私有方法,我们无法直接访问,但是我们可以访问它的键盘,开机键,显示器,这些就是公共方法。
    • 继承
      软件复用的重要手段,子类继承父类,可以直接复用父类的属性和方法。
    • 多态
      子类对象直接赋给父类变量,运行的时候表现为子类的特性。

    抽象也是面向对象的重要组成之一,但是不是基本特征。抽象是抽取我们当前目标所需要的东西,排除一些无关的信息。

    Java面向对象特征


    在初遇章节我们就谈过Java的面向对象特征,我们这里再次谈谈Java面向对象特征。

    • 一切皆是对象
      除了8个基本数据类型,一切皆是对象。对象实现了数据和操作的结合,是Java的核心,具备唯一性,每个对象都有一个标识来引用,如果失去这个引用,那么这个对象将会变成垃圾,然后会被虚拟机回收掉。Java中不允许直接访问对象,而是通过一个引用(也有一种称呼为句柄)来操作对象。就如同设计一台电视机,电视机上没有任何按钮,只能通过遥控来操作电视机。而这个遥控就是引用(句柄),电视机就是对象。
      如 Person p = new Person();


      image.png

      p就是一个引用变量,其实就是C语言中的指针,只是Java友好的将这个指针封装起来了,不需要繁琐的去操作它。p中存储的是Person的地址,当访问p引用变量的成员变量和方法,其实就是访问Person的成员变量和方法。

    • 类和对象
      对象也称为实例instance,对象的抽象化是类,类的具体化是对象。Java语言使用class来定义对象,通过成员变量来描述对象的数据,通过方法来描述对象的行为特征。类之间的关系一般有两种:
      1. 一般->特殊关系(is a),Java中使用extends来表示这种特殊的关系,即继承关系。
        发生在继承关系常见的一个概念是重写(Overrride),重写必须符合规则式:两同两小一大。即,方法名,形参列表相同;返回值类型要比父类的返回值类型更小或者相等;子类抛出的异常必须比父类的异常更小或者相等(不能一代不如一代);子类的访问权限必须比父类的相等或者更大。
        这里需要注意的是当父类的方法是private修饰时,子类是不能访问的。
      2. 整体->部分关系(has a),组合关系,即Java中一个类里面保存了另一个类的引用来实现这种关系。

    修饰符


    • private 私有的(类访问权限)
    • default 默认(包访问权限)
    • protected 子类访问权限
    • public 公共访问权限

    this和super


    面向对象离不开this和super,这里我们分析下这两个关键字

    • this
      this关键字指向调用该方法的对象,一般会出现在构造器和方法中。我们知道一种特殊的方法static修饰的,就是静态方法,调用静态方法可以使用类对象,所以this无法指向调用该方法的对象,所以静态方法里面不能使用this,同样,静态方法中不能使用非静态成员变量。
    • super
      super是用来子类调用父类的方法或者构造方法的。和this一样,super也不能应用在静态方法中
      子类调用父类构造器过程是:
      1. 子类构造器执行体的第一行使用super显式调用父类构造器,系统会根据super传入的实例列表调用父类对应的构造器。
      2. 子类构造器执行体的第一行使用this显式的调用本类的重载构造器,执行本类的另一个构造器时即会调用父类构造器。
      3. 子类构造器既没有super,也没有this,系统将会执行子类构造器之前,隐式的调用父类的无参构造器

    final修饰符


    final用来修饰类、变量、方法表示该类、变量、方法不可改变。

    • final修饰变量
      final修饰变量一旦获得初始值后是不能改变的。如下图,我们编译器在编译过程中就会报错The final local variable a may already have been assigned
      image.png
      关于final修饰成员变量,必须显式的初始化。
      1.普通成员变量,必须在初始化块(代码块)、声明时或者构造器中初始化。
      1. 静态成员变量,必须在静态代码块、声明时初始化。
        其实final的不可改变也不是绝对的,这就是final修饰基本类型变量和引用类型变量的区别,修饰引用类型时,只要保证引用类型的地址不变,而引用的这个对象完全可以改变。
    • final方法
      final方法不能被重写,如果父类不想让子类继承某个方法,可以定义为final类型。
    • final类
      final类不能有子类

    聊聊Lambda表达式


    Lambda表达式是Java8新增的一个重要功能,是大家期待已久的,它使得代码更为的简洁、直观,接下来让我们了解下Lambda表达式的功能。

    • 组成部分
      1. 形参列表。允许省略参数类型,如果是一个参数甚至可以省略圆括号
      2. 箭头。(->)必须是英文的划线号和大于号组成
      3. 代码块。 如果代码块只有一条语句,可以省略花括号。如果只有一条返回语句,return关键字也可以省略。
        比如说,我们可以创建一个线程类
    Thread thread = new Thread((Runnable) ()->{
                System.out.println(Thread.currentThread().getName()+"-run--");
            });
    

    这样写也是可以的

    Thread thread = new Thread(()->System.out.println(Thread.currentThread().getName()+"-run--"));
    
    • 方法引用和构造器引用
    @FunctionalInterface
        interface Converter{
            Integer converter(String from);
        }
    Converter converter = from ->Integer.valueOf(from);
    

    上面代码其实就是对接口Converter的一个实现,然后把实现的地址赋给了引用变量converter。上面的代码还可以简写成

    Converter converter = Integer::valueOf;
    

    调用converter.converter("5");也就是调用Integer.valueOf("5");
    Lambda还有很多有意思的写法,这就需要通过实践中去探索了。

    实战


    没有实战的概念就是耍流氓。

    • 一个比较坑的问题
    public class StaticThreadDemo implements Runnable{
        public static Integer i = new Integer(0);
        @Override
        public void run() {
            while(true){
                synchronized (i) {
                    if(i<100){
                        i++;
                        System.out.println("i="+i);
                    }else{
                        break;
                    }
                }
            }
        }
        public static void main(String[] args) {
            Thread t1 = new Thread(new StaticThreadDemo());
            Thread t2 = new Thread(new StaticThreadDemo());
            t1.start();
            t2.start();
        }
    }
    

    问题输出的结果是啥?按顺序1-100?重复输出1-100?无序的1-100?
    运行的结果是:无序的,有重复,有确实的打印1-100。
    就是说,这是个线程不安全的程序。那么为什么会导致这种情况呢?
    分析:
    synchronized 锁对象的问题。我们知道,静态变量和类信息(区分类对象)都是存放在我们的方法区中(因此静态变量属于类本身而不属于实例),我们可以认为是线程共享的,唯一的。那,我们应该要理解的是引用i对应的对象是否被偷换的问题,如果没有变化,那么,i肯定是线程安全的。我们编译下这段代码。

    public class StaticThreadDemo implements Runnable {
        public static Integer i = new Integer(0);
    
        public StaticThreadDemo() {
        }
    
        public void run() {
            while(true) {
                Integer var1 = i;
                synchronized(i) {
                    if(i.intValue() >= 100) {
                        return;
                    }
                    Integer e = i;
                    i = Integer.valueOf(i.intValue() + 1);
                    System.out.println("i=" + i);
                }
            }
        }
    
        public static void main(String[] args) {
            Thread t1 = new Thread(new StaticThreadDemo());
            Thread t2 = new Thread(new StaticThreadDemo());
            t1.start();
            t2.start();
        }
    }
    

    我们发现:Integer要获取它的数据需要通过intValue() 方法,那么intValue()方法干了件什么事呢?查看Integer对象源码

    private final int value;
      public int intValue() {
            return value;
        }
    

    我们上面说过,对象的数据使用成员变量来描述,而这个成员变量是私有的,我们只能通过它的方法来获取。
    i++分解成了两句

     Integer e = i;
     i = Integer.valueOf(i.intValue() + 1);
    

    第一句我们比较好理解,就是用一个新的对象保存旧的数据,而第二句才是重点,我们先看下Interger的静态方法valueOf

       public static Integer valueOf(int i) {
            if (i >= IntegerCache.low && i <= IntegerCache.high)
                return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }
    

    解释一下这段代码,就是当i的字段在-128和127之间的话,从IntegerCache缓存里面获取,如果在区间之外的话,重新new一个对象,当然,缓存里面其实也是new Integer(i);所以说i的对象发生了改变了,因此,synchronized锁不住对象了。
    我们可以这样理解这个流程,线程t1获取锁对象,进入run方法,执行i++后,锁对象发生了改变,这个时候线程t1,t2一起争取新的锁对象,由于这一步和打印语句并行,所以存在线程安全问题。


    image.png

    这里提一下Integer内部类IntegerCache缓存对象问题,在Java5加入了自动装箱和自动拆箱后(实现原理就是valueOf方法),如果int值在-128和127之间,Java不会new一个对象,而是直接从缓存里面获取了,这就有了面试题Integer a =127;Integer b = 127;Integer c =128;Integer d = 128;
    System.out.println(a==b); System.out.println(c==d);

    尾声

    通过本章节,我们说到了面向对象的基本特性与面向过程的优势所在,然后阐述了Java面向对象的特征,引出了引用数据类型。后面我们说到了一些修饰符,如访问权限修饰符,关键字等。还提到了Java8新增的Lambda表达式的应用。总之,Java面向对象博大精深,不是一篇文章就能说得清楚的,如果要深入学习,我们还需要阅读相关的书籍。在最后,我举了一个多线程安全问题的案例,详细分析了Integer对象在i++过程中的实际操作以及对象之间的变化,希望能帮到大家进一步了解面向对象思想。

    相关文章

      网友评论

          本文标题:Java基础系列之面向对象

          本文链接:https://www.haomeiwen.com/subject/akmdfqtx.html