访问控制权限
访问控制权限又称为封装
,它是面向对象三大特性中的一种,我之前在学习过程中经常会忽略封装,心想这不就是一个访问修饰符么,怎么就是三大特性的必要条件了?后来我才知道,如果你信任的下属对你隐瞒 bug,你是根本不知道的。
访问控制权限其实最核心就是一点:只对需要的类可见。
Java 中的访问权限共有四种,分别是 public、protected、default、private,它们的可见性如下
继承
继承是所有 OOP(Object Oriented Programming)
语言和 Java 语言都不可或缺的一部分。只要我们创建了一个类,就隐式的继承自 Object
父类,只不过没有指定。如果你显示指定了父类,那么你继承于父类,而你的父类继承于 Object 类。
继承的关键字是 extends
,如上图所示,如果使用了 extends 显示指定了继承,那么我们可以说 Father 是父类,而 Son 是子类,用代码表示如下
class Father{} class Son extends Father{}
继承双方拥有某种共性的特征
class Father{ public void feature(){ System.out.println("父亲的特征"); } } class Son extends Father { }
如果 Son 没有实现自己的方法的话,那么默认就是用的是父类的 feature
方法。如果子类实现了自己的 feature 方法,那么就相当于是重写了父类的 feature 方法,这也是我们上面提到的重写了。
多态
多态指的是同一个行为具有多个不同表现形式。是指一个类实例(对象)的相同方法在不同情形下具有不同表现形式。封装和继承是多态的基础,也就是说,多态只是一种表现形式而已。
如何实现多态?多态的实现具有三种充要条件
- 继承
- 重写父类方法
- 父类引用指向子类对象
比如下面这段代码
public class Fruit { int num; public void eat(){ System.out.println("eat Fruit"); } } public class Apple extends Fruit{ @Override public void eat() { super.num = 10; System.out.println("eat " + num + " Apple"); } public static void main(String[] args) { Fruit fruit = new Apple(); fruit.eat(); } }
你可以发现 main
方法中有一个很神奇的地方,Fruit fruit = new Apple()
,Fruit 类型的对象竟然指向了 Apple 对象的引用,这其实就是多态 -> 父类引用指向子类对象,因为 Apple 继承于 Fruit,并且重写了 eat 方法,所以能够表现出来多种状态的形式。
组合
组合其实不难理解,就是将对象引用置于新类中即可。组合也是一种提高类的复用性的一种方式。如果你想让类具有更多的扩展功能,你需要记住一句话多用组合,少用继承。
public class SoccerPlayer { private String name; private Soccer soccer; } public class Soccer { private String soccerName; }
代码中 SoccerPlayer 引用了 Soccer 类,通过引用 Soccer 类,来达到调用 soccer 中的属性和方法。
组合和继承是有区别的,它们的主要区别如下。
关于继承和组合孰优孰劣的争论没有结果,只要发挥各自的长处和优点即可,一般情况下,组合和继承也是一对可以连用的好兄弟。
代理
除了继承和组合外,另外一种值得探讨的关系模型称为 代理
。代理的大致描述是,A 想要调用 B 类的方法,A 不直接调用,A 会在自己的类中创建一个 B 对象的代理,再由代理调用 B 的方法。例如下面代码:
public class Destination { public void todo(){ System.out.println("control..."); } } public class Device { private String name; private Destination destination; private DeviceController deviceController; public void control(Destination destination){ destination.todo(); } } public class DeviceController { private Device name; private Destination destination; public void control(Destination destination){ destination.todo(); } }
关于深入理解代理的文章,可以参考
向上转型
向上转型代表了父类与子类之间的关系,其实父类和子类之间不仅仅有向上转型,还有向下转型,它们的转型后的范围不一样
向上转型
:通过子类对象(小范围)转化为父类对象(大范围),这种转换是自动完成的,不用强制。向下转型
: 通过父类对象(大范围)实例化子类对象(小范围),这种转换不是自动完成的,需要强制指定。
static
static 是 Java 中的关键字,它的意思是 静态的
,static 可以用来修饰成员变量和方法,static 用在没有创建对象的情况下调用 方法/变量。
- 用 static 声明的成员变量为静态成员变量,也成为类变量。类变量的生命周期和类相同,在整个应用程序执行期间都有效。
static String name = "cxuan";
- 使用 static 修饰的方法称为静态方法,静态方法能够直接使用类名.方法名 进行调用。由于静态方法不依赖于任何对象就可以直接访问,因此对于静态方法来说,是没有 this 关键字的,实例变量都会有 this 关键字。在静态方法中不能访问类的非静态成员变量和非静态方法,
static void printMessage(){ System.out.println("cxuan is writing the article"); }
static 除了修饰属性和方法外,还有静态代码块
的功能,可用于类的初始化操作。进而提升程序的性能。
public class StaicBlock { static{ System.out.println("I'm A static code block"); } }
由于静态代码块随着类的加载而执行,因此,很多时候会将只需要进行一次的初始化操作放在 static 代码块中进行。
关于 static 关键字的深入理解用法,可以参考笔者的这篇文章 一个 static 还能难得住我?强烈建议学完 Java 基础之后阅读。
final
final 的意思是最后的、最终的,它可以修饰类、属性和方法。
- final 修饰类时,表明这个类不能被继承。final 类中的成员变量可以根据需要设为 final,但是要注意 final 类中的所有成员方法都会被隐式地指定为 final 方法。
class Parent {} final class Person extends Parent{} //可以继承Parent类 class Child extends Person{} //不能继承Person类
- final 修饰方法时,表明这个方法不能被任何子类重写,因此,如果只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为 final。
class Parent { // final修饰的方法,不可以被覆盖,但可以继承使用 public final void method1(){} //这个方法不可以重写 public void method2(){} } class Child extends Parent { //可以重写method2方法 public final void method2(){} }
- final 修饰变量分为两种情况,一种是修饰基本数据类型,表示数据类型的值不能被修改;一种是修饰引用类型,表示对其初始化之后便不能再让其指向另一个对象。
final int i = 20; i = 30; //赋值报错,final修饰的变量只能赋值一次
在 Java 中,与 final 、finally 和 finalize 并成为最后的三兄弟。关于这三个关键字的详细用法,你可以参考阅读作者的这篇文章 看完这篇 final、finally 和 finalize 和面试官扯皮就没问题了
接口和抽象类
接口
接口相当于就是对外的一种约定和标准,这里拿操作系统举例子,为什么会有操作系统?就会为了屏蔽软件的复杂性和硬件的简单性之间的差异,为软件提供统一的标准。
在 Java 语言中,接口是由 interface
关键字来表示的,比如我们可以向下面这样定义一个接口
public interface CxuanGoodJob {}
比如我们定义了一个 CxuanGoodJob 的接口,然后你就可以在其内部定义 cxuan 做的好的那些事情,比如 cxuan 写的文章不错。
public interface CxuanGoodJob { void writeWell(); }
这里隐含了一些接口的特征:
interface
接口是一个完全抽象的类,他不会提供任何方法的实现,只是会进行方法的定义。- 接口中只能使用两种访问修饰符,一种是
public
,它对整个项目可见;一种是default
缺省值,它只具有包访问权限。 - 接口只提供方法的定义,接口没有实现,但是接口可以被其他类实现。也就是说,实现接口的类需要提供方法的实现,实现接口使用
implements
关键字来表示,一个接口可以有多个实现。
class CXuanWriteWell implements CxuanGoodJob{ @Override public void writeWell() { System.out.println("Cxuan write Java is vary well"); } }
- 接口不能被实例化,所以接口中不能有任何构造方法,你定义构造方法编译会出错。
- 接口的实现比如实现接口的全部方法,否则必须定义为
抽象类
,这就是我们下面要说的内容
抽象类
抽象类是一种抽象能力弱于接口的类,在 Java 中,抽象类使用 abstract
关键字来表示。如果把接口形容为狗这个物种,那么抽象类可以说是毛发是白色、小体的品种,而实现类可以是具体的类,比如说是博美、泰迪等。你可以像下面这样定义抽象类
public interface Dog { void FurColor(); } abstract class WhiteDog implements Dog{ public void FurColor(){ System.out.println("Fur is white"); } abstract void SmallBody(); }
在抽象类中,具有如下特征
- 如果一个类中有抽象方法,那么这个类一定是抽象类,也就是说,使用关键字
abstract
修饰的方法一定是抽象方法,具有抽象方法的类一定是抽象类。实现类方法中只有方法具体的实现。 - 抽象类中不一定只有抽象方法,抽象类中也可以有具体的方法,你可以自己去选择是否实现这些方法。
- 抽象类中的约束不像接口那么严格,你可以在抽象类中定义 构造方法、抽象方法、普通属性、方法、静态属性和静态方法
- 抽象类和接口一样不能被实例化,实例化只能实例化
具体的类
。
异常
异常是程序经常会出现的,发现错误的最佳时机是在编译阶段,也就是你试图在运行程序之前。但是,在编译期间并不能找到所有的错误,有一些 NullPointerException
和 ClassNotFoundException
异常在编译期找不到,这些异常是 RuntimeException 运行时异常,这些异常往往在运行时才能被发现。
我们写 Java 程序经常会出现两种问题,一种是 java.lang.Exception ,一种是 java.lang.Error,都用来表示出现了异常情况,下面就针对这两种概念进行理解。
认识 Exception
Exception
位于 java.lang
包下,它是一种顶级接口,继承于 Throwable
类,Exception 类及其子类都是 Throwable 的组成条件,是程序出现的合理情况。
在认识 Exception 之前,有必要先了解一下什么是 Throwable
。
什么是 Throwable
Throwable 类是 Java 语言中所有错误(errors)
和异常(exceptions)
的父类。只有继承于 Throwable 的类或者其子类才能够被抛出,还有一种方式是带有 Java 中的 @throw
注解的类也可以抛出。
在Java规范中,对非受查异常和受查异常的定义是这样的:
The unchecked exception classes are the run-time exception classes and the error classes.
The checked exception classes are all exception classes other than the unchecked exception classes. That is, the checked exception classes are
Throwable
and all its subclasses other thanRuntimeException
and its subclasses andError
and its subclasses.
也就是说,除了 RuntimeException
和其子类,以及error
和其子类,其它的所有异常都是 checkedException
。
那么,按照这种逻辑关系,我们可以对 Throwable 及其子类进行归类分析
可以看到,Throwable 位于异常和错误的最顶层,我们查看 Throwable 类中发现它的方法和属性有很多,我们只讨论其中几个比较常用的
// 返回抛出异常的详细信息 public string getMessage(); public string getLocalizedMessage(); //返回异常发生时的简要描述 public public String toString(); // 打印异常信息到标准输出流上 public void printStackTrace(); public void printStackTrace(PrintStream s); public void printStackTrace(PrintWriter s) // 记录栈帧的的当前状态 public synchronized Throwable fillInStackTrace();
此外,因为 Throwable 的父类也是 Object
,所以常用的方法还有继承其父类的getClass()
和 getName()
方法。
常见的 Exception
下面我们回到 Exception 的探讨上来,现在你知道了 Exception 的父类是 Throwable,并且 Exception 有两种异常,一种是 RuntimeException
;一种是 CheckedException
,这两种异常都应该去捕获
。
下面列出了一些 Java 中常见的异常及其分类,这块面试官也可能让你举出几个常见的异常情况并将其分类
RuntimeException
UncheckedException
与 Exception 有关的 Java 关键字
那么 Java 中是如何处理这些异常的呢?在 Java 中有这几个关键字 throws、throw、try、finally、catch 下面我们分别来探讨一下
throws 和 throw
在 Java 中,异常也就是一个对象,它能够被程序员自定义抛出或者应用程序抛出,必须借助于 throws
和 throw
语句来定义抛出异常。
throws 和 throw 通常是成对出现的,例如
static void cacheException() throws Exception{ throw new Exception(); }
throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。throws 语句用在方法声明后面,表示再抛出异常,由该方法的调用者来处理。
throws 主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常。throw 是具体向外抛异常的动作,所以它是抛出一个异常实例。
try 、finally 、catch
这三个关键字主要有下面几种组合方式 try...catch 、try...finally、try...catch...finally。
try...catch 表示对某一段代码可能抛出异常进行的捕获,如下
static void cacheException() throws Exception{ try { System.out.println("1"); }catch (Exception e){ e.printStackTrace(); } }
try...finally 表示对一段代码不管执行情况如何,都会走 finally 中的代码
static void cacheException() throws Exception{ for (int i = 0; i < 5; i++) { System.out.println("enter: i=" + i); try { System.out.println("execute: i=" + i); continue; } finally { System.out.println("leave: i=" + i); } } }
try...catch...finally 也是一样的,表示对异常捕获后,再走 finally 中的代码逻辑。
什么是 Error
Error 表示程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。这些错误是不可检查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况,比如 OutOfMemoryError
和 StackOverflowError
异常的出现会有几种情况,这里需要先介绍一下 Java 内存模型 JDK1.7。
其中包括两部分,由所有线程共享的数据区和线程隔离的数据区组成,在上面的 Java 内存模型中,只有程序计数器是不会发生 OutOfMemoryError
情况的区域,程序计数器控制着计算机指令的分支、循环、跳转、异常处理和线程恢复,并且程序计数器是每个线程私有的。
什么是线程私有:表示的就是各条线程之间互不影响,独立存储的内存区域。
如果应用程序执行的是 Java 方法,那么这个计数器记录的就是虚拟机字节码
指令的地址;如果正在执行的是 Native
方法,这个计数器值则为空(Undefined)
。
除了程序计数器外,其他区域:方法区(Method Area)
、虚拟机栈(VM Stack)
、本地方法栈(Native Method Stack)
和 堆(Heap)
都是可能发生 OutOfMemoryError 的区域。
- 虚拟机栈:如果线程请求的栈深度大于虚拟机栈所允许的深度,将会出现
StackOverflowError
异常;如果虚拟机动态扩展无法申请到足够的内存,将出现OutOfMemoryError
。 - 本地方法栈和虚拟机栈一样
- 堆:Java 堆可以处于物理上不连续,逻辑上连续,就像我们的磁盘空间一样,如果堆中没有内存完成实例分配,并且堆无法扩展时,将会抛出 OutOfMemoryError。
- 方法区:方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
在 Java 中,你可以把异常理解为是一种能够提高你程序健壮性的机制,它能够让你在编写代码中注意这些问题,也可以说,如果你写代码不会注意这些异常情况,你是无法成为一位硬核程序员的。