在初遇章节我们就谈到过Java是一门面向对象的语言,那么什么是面向对象呢?既然有面向对象语言,是否就有其他的语言?面向对象又能给我么带来什么好处呢?接下来,我们将在这个章节探讨下面向对象。
面向过程和面向对象
在目前的软件开发领域有两种主流的开发方法:结构化开发方法(面向过程)和面向对象开发方法。早期的编程语言C、Basic、Pascal等都是结构化编程语言,随着时代的变迁,软件的发展,人们发现了一种更好的可复用、可扩展和可维护的的方法,即面向对象,代表语言有C++,C#,Ruby,Java等。
- 面向过程
主张按功能来设计程序,特点是:自上而下,逐步求精,模块化等。结构化程序设计的最小单元是函数,每个函数都负责完成一个功能。局限性有两点:一,设计不够直观,与人类习惯思维不一致;二,适应性差,可扩展性不强。 - 面向对象
更优秀的程序设计思想,基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。最小单位是类,由类可以生成系统中多个对象。
面向对象的基本特征
- 封装
隐藏细节,通过公共方法暴露出该对象的功能。比如说一台电脑,我们在不拆机的情况下看不到里面的主板,cpu,内存条,这些好比是私有方法,我们无法直接访问,但是我们可以访问它的键盘,开机键,显示器,这些就是公共方法。 - 继承
软件复用的重要手段,子类继承父类,可以直接复用父类的属性和方法。 - 多态
子类对象直接赋给父类变量,运行的时候表现为子类的特性。
抽象也是面向对象的重要组成之一,但是不是基本特征。抽象是抽取我们当前目标所需要的东西,排除一些无关的信息。
Java面向对象特征
在初遇章节我们就谈过Java的面向对象特征,我们这里再次谈谈Java面向对象特征。
-
一切皆是对象
除了8个基本数据类型,一切皆是对象。对象实现了数据和操作的结合,是Java的核心,具备唯一性,每个对象都有一个标识来引用,如果失去这个引用,那么这个对象将会变成垃圾,然后会被虚拟机回收掉。Java中不允许直接访问对象,而是通过一个引用(也有一种称呼为句柄)来操作对象。就如同设计一台电视机,电视机上没有任何按钮,只能通过遥控来操作电视机。而这个遥控就是引用(句柄),电视机就是对象。
如 Person p = new Person();
p就是一个引用变量,其实就是C语言中的指针,只是Java友好的将这个指针封装起来了,不需要繁琐的去操作它。p中存储的是Person的地址,当访问p引用变量的成员变量和方法,其实就是访问Person的成员变量和方法。
- 类和对象
对象也称为实例instance,对象的抽象化是类,类的具体化是对象。Java语言使用class来定义对象,通过成员变量来描述对象的数据,通过方法来描述对象的行为特征。类之间的关系一般有两种:- 一般->特殊关系(is a),Java中使用extends来表示这种特殊的关系,即继承关系。
发生在继承关系常见的一个概念是重写(Overrride),重写必须符合规则式:两同两小一大。即,方法名,形参列表相同;返回值类型要比父类的返回值类型更小或者相等;子类抛出的异常必须比父类的异常更小或者相等(不能一代不如一代);子类的访问权限必须比父类的相等或者更大。
这里需要注意的是当父类的方法是private修饰时,子类是不能访问的。 - 整体->部分关系(has a),组合关系,即Java中一个类里面保存了另一个类的引用来实现这种关系。
- 一般->特殊关系(is a),Java中使用extends来表示这种特殊的关系,即继承关系。
修饰符
- private 私有的(类访问权限)
- default 默认(包访问权限)
- protected 子类访问权限
- public 公共访问权限
this和super
面向对象离不开this和super,这里我们分析下这两个关键字
- this
this关键字指向调用该方法的对象,一般会出现在构造器和方法中。我们知道一种特殊的方法static修饰的,就是静态方法,调用静态方法可以使用类对象,所以this无法指向调用该方法的对象,所以静态方法里面不能使用this,同样,静态方法中不能使用非静态成员变量。 - super
super是用来子类调用父类的方法或者构造方法的。和this一样,super也不能应用在静态方法中。
子类调用父类构造器过程是:- 子类构造器执行体的第一行使用super显式调用父类构造器,系统会根据super传入的实例列表调用父类对应的构造器。
- 子类构造器执行体的第一行使用this显式的调用本类的重载构造器,执行本类的另一个构造器时即会调用父类构造器。
- 子类构造器既没有super,也没有this,系统将会执行子类构造器之前,隐式的调用父类的无参构造器
final修饰符
final用来修饰类、变量、方法表示该类、变量、方法不可改变。
- final修饰变量
final修饰变量一旦获得初始值后是不能改变的。如下图,我们编译器在编译过程中就会报错The final local variable a may already have been assigned
。
关于final修饰成员变量,必须显式的初始化。
1.普通成员变量,必须在初始化块(代码块)、声明时或者构造器中初始化。- 静态成员变量,必须在静态代码块、声明时初始化。
其实final的不可改变也不是绝对的,这就是final修饰基本类型变量和引用类型变量的区别,修饰引用类型时,只要保证引用类型的地址不变,而引用的这个对象完全可以改变。
- 静态成员变量,必须在静态代码块、声明时初始化。
- final方法
final方法不能被重写,如果父类不想让子类继承某个方法,可以定义为final类型。 - final类
final类不能有子类
聊聊Lambda表达式
Lambda表达式是Java8新增的一个重要功能,是大家期待已久的,它使得代码更为的简洁、直观,接下来让我们了解下Lambda表达式的功能。
- 组成部分
- 形参列表。允许省略参数类型,如果是一个参数甚至可以省略圆括号
- 箭头。(->)必须是英文的划线号和大于号组成
- 代码块。 如果代码块只有一条语句,可以省略花括号。如果只有一条返回语句,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一起争取新的锁对象,由于这一步和打印语句并行,所以存在线程安全问题。
这里提一下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++过程中的实际操作以及对象之间的变化,希望能帮到大家进一步了解面向对象思想。