Java基础整理
Java SE基础是Java学习中的基石,后续的学习基本都需要依靠这些基础内容,如果这一部分的内容掌握不牢固,就会导致后续学习内容的晦涩难懂。
基于以上原因,加上自我感觉Java SE的内容可能只掌握了60%,因此完成这份补完计划十分重要。Java SE大体可以分为三个部分:
- Java基础语法:包括面向对象编程、集合、异常、反射等
- 操作系统相关:多线程,IO流等
- 数据结构与算法:表、树、哈希、KMP、动态规划等
参考文档&教程&书籍
- Java SE 笔记
- Java教程-菜鸟教程
- 《Java 核心技术卷》
Java基础语法
Java语法与使用规范
对于Java的语法和使用规范是十分基础的东西,具体的内容可以参照文档&教程&书籍中的内容进行了解学习。
主要的内容包括:Java中的关键字,变量和常量,注释,基本数据类型以及数据类型之间的转换,运算符,流程控制。
了解完Java的语法和使用规范之后,就可以进行下一部分。
Java对象与多态
面向对象程序设计
面向对象程序设计(OOP)是当今主流的程序设计范式,而Java是面向对象的,必须熟悉OOP才能编写Java程序。
在使用OOP时,需要从对象的三个主要特性入手,分别是行为,状态(对象的状态不会自发改变,必须通过调用方法实现)和标识。对象这三个主要特性在彼此之间互相影响。
传统的结构化程序设计通过设计一系列的过程(或者说算法)来求解问题,一旦确定了过程,就要考虑存储数据的方式。这也是Nikalus Wirth编著的《算法+数据结构=程序》中,标题第一位是算法,第二位是数据结构的原因
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。实际上,在面向对象中,只要对象可以满足要求,就不关心功能的具体实现过程,只需要满足用户的需求。 ——Java核心技术卷
Java中的类
Java中的对象都基于类创建,这些类可以是Java自带的类,也可以是程序员自己编写的类。简单来说,类是对象的一个模板,有自己的属性,包括成员变量,成员方法等,我们可以通过调用类的成员方法来进行一些操作。(这里可以看下菜鸟教程:Java对象和类)
在使用面向对象程序设计时,经常需要分析用户的需求,识别出需要使用和设计的类。识别类最简单的方法就是在分析问题的过程中寻找名词,这些名词就代表着类,而方法由动词对应。当然,在实际开发中,更多的还是得依赖于个人的开发经验。
在Java中,类的加载机制也需要注意。类并不是在一开始就全部加载好,而是在需要时才会去加载,目的是提高速度,如以下情况下会加载类:
- 访问类的静态变量,或者为静态变量赋值
- new创建对象的实例
- 调用类的静态方法
- 子类初始化
- 以及特殊情况
类与类之间,存在着三种常见的关系:
- 依赖(uses-a):如果一个类的方法操纵另一个类的对象,就可以说一个类依赖于另一个类。在开发中,应该尽可能将相互依赖的类减少,使用术语来说,就是让类之间的耦合度最低。
- 聚合(has-a):这个关系其实可以从英语出发,就是一个对象中存在另外一些对象,这样就可以说类与类之间存在聚合关系。其实我们更应该理解为包含。
- 继承(is-a):继承是一种用于表示特殊与一般关系的。可以理解为社会中的父子关系。(注意,继承内容是Java中非常重要的部分,笔者自身已经反复学习理解过多次,如果新手读者建议找找教程视频深刻学习体会)
抽象类和接口
抽象类和接口,其实就是类的抽象再抽象。
对于类的初步抽象,就是保留特征而不保留具体形态。比如对于方法,在抽象类中我可以定义却不去实现,保留给子类实现。
对抽象类的抽象,也即是对类的深度抽象,就是接口了。严格说它甚至不是一个类,只代表了某个功能,只包含对方法的定义。接口的功能需要由主体——也就是类来实现。
包装类和枚举类
包装类
虽然说Java是一种面向对象的语言,不过Java也不是完全面向对象的。例如Java中的基本数据类型就不是对象。当我们想通过对象的方式去使用基本数据类型时,就需要将其转换为相对应的对象。Java中提供这一服务的类就称之为包装类,将基本数据类型转换为对象的过程称为打包(装箱),反过来的过程称为拆包(拆箱)。这两个过程在Java中可以自动完成,也可以由程序员自己编写。
枚举类
有时候,我们想为对象添加一个描述或者一种状态(例如衣服的尺码有小、中、大、加大等,人的状态有睡觉、工作,吃饭等)。如果此时我们使用字符串来存储这一部分数据,就有可能出现外部传来的数据并不是我们想要的。这个时候,就可以使用枚举类来解决这个问题,这样就只能使用我们定义好的状态。
//服装尺码的枚举类
//实际上,这句代码定义了一个类,这个类有这四个实例,所以在使用枚举类时,尽量不要构造新对象
public enum Size {SMALL , MEDIUM , LARGE , EXTRA_LARGE};
此外,所有枚举类都是Enum类的子类,也就是说继承了Enum类的许多方法可以使用。
Java的异常机制与异常处理
一个程序的使用过程中不可能完全不存在BUG,代码跑不动才是常态。这是很正常的现象,关键是要在出现BUG之后优化程序,使之不断改进,不断趋于完美。
在Java 核心技术卷中建议在出现异常时,至少应该做到:
- 向用户通告错误
- 保存所有的操作结果
- 允许用户以适当的形式退出程序
异常分类
从上图其实可以很清晰的看出,Java中的异常其实也是一个类,继承自Throwable类,分为两种:Error类和Exception,其中Exception类还有IOException和RuntimeException两个子类。当然,如果现有的类不足以满足用户的需求,同样可以自己编写需要的子类。
- Error类描述的是Java运行是系统的内部错误和资源耗尽错误。如果出现了这样的内部错误,程序除了通告给用户同时尽力安全地终止程序外,其他的也无能为力。这种情况在实际开发很少出现。
- Exception类是在开发中需要重点关注的部分。一般来说,由程序错误导致的异常术语RuntimeException;如果程序本身没有问题,不过由于 I/O 错误这类问题导致的异常属于IOException。
- RuntimeException的异常还包括:错误的类型转换,数组访问越界,访问空指针
- IOException的异常还包括:试图的文件尾部后面读取数据,试图打开一个错误格式的URL,试图根据给定的字符串查找Class对象,但是字符串表示的类并不存在
异常的抛出,捕获及自定义
抛出异常
如果调用方法时因为传入错误的参数,进而导致程序无法正常运行,此时应该手动抛出一个异常来终止程序,然后报告上一级方法执行出现问题。
public static void main(String[] args) {
try {
test(1, 0);
} catch (Exception e) { //捕获方法中会出现的异常
e.printStackTrace();
}
}
//声明抛出的异常类型
private static int test(int a, int b) throws Exception {
if(b == 0) throw new Exception("0不能做除数!"); //创建异常对象并抛出异常
return a/b; //抛出异常会终止代码运行
}
如果调用编译时异常的方法,但是依然不想去处理,可以同样的在方法上声明 throws 来继续交给上一级处理
public static void main(String[] args) throws Exception { //出现异常就再往上抛,而不是在此方法内处理
test(1, 0);
}
private static int test(int a, int b) throws Exception { //声明抛出的异常类型
if(b == 0) throw new Exception("0不能做除数!"); //创建异常对象并抛出异常
return a/b;
}
出现类似以上代码的情况,连main方法都声明抛出异常时,这时出现的异常就会交给JVM处理,JVM会直接终止程序并在控制台打印栈追踪信息(这也是默认的处理方法)
捕获异常
抛出一个异常十分容易,只要将它抛出就行了。不过,一些代码必须捕获异常。
try{
some code...
}catch(Exception e){
some code...
}
try/catch语句是捕获一个异常必须设置的。如果try代码块中抛出了一个在catch子句中说明的异常,那么,首先程序会跳过try代码块中的其余代码,执行catch中的处理器代码。如果没有出现任何异常,则不会执行catch中的代码。如果出现没有声明的异常类型,那么程序就会立刻退出。
编译器严格执行throws说明符,如果调用一个抛出已检查异常(非派生于Error类和RuntimeException类的异常)的方法,就必须对它进行处理,或者将其传递出去(即抛出)。
通常,应捕获那些知道如何处理的异常,传递那些不知道怎么处理的异常。如果想将异常传出去,就必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。
阅读Java API文档,以便知道每个方法可能会抛出的异常,然后决定是自己处理,还是添加到throws列表里。对后一种,不必犹豫,将异常直接交给能够胜任的处理器进行处理会比压制对它的处理更好。
——《Java核心技术卷》
对于捕获多个异常来说,就是在try/catch语句基础上,再多加入声明不同异常类型的catch语句。
此外,还可以在catch语句中再抛出一个异常,这样做的目的一般是改变异常的类型。
自定义异常
异常的自定义非常简单,只要继承部分的知识掌握的足够,自定义异常的编写更多是对父类方法的重写。
Java中的断言,日志和调试
断言机制
在程序设计中,代码运行测试是必不可少的,这就要求在程序中插入测试代码。一般的测试代码在测试完成后不会自动删除,大量的测试代码会导致程序的运行变慢。
断言机制就是允许在测试时向代码插入检查语句,到代码发布时,这些插入的语句将会被自动的移走。Java中引入 assert 关键字来使用断言。
不过,在现在的开发中,断言并不推荐使用。详见:Java陷阱之assert关键字
泛型程序设计
泛型的概念
泛型,其实就是为了编写的代码可以被不同类型对象所使用。
在没使用泛型之前,代码可能是这样写的
public class List{
private String[] paraList;
public String get(int i){...};
public void add(String para){...};
.....
}
对于这样一个List类,在使用时只能添加String类型的数据,如果要使用其他类型,好像得重写一下。有人可能会想到Object类,它是所有类的超类,修改如下
public class List{
private Object[] paraList;
public Object get(int i){...};
public void add(Object para){...};
.....
}
可是,这里一样会出现两个问题:
-
获取一个值时必须进行强制类型转换
String s = (String)list.get(0);
-
可以在数组中添加任何类的对象,容易导致一些无法预料的错误。
基于以上原因,泛型的作用就凸显出来了。相同代码,完全可以这样编写(这里的T只能是类,不可以使用基本数据类型,可以使用包装类)
public class List<T>{
private T[] paraList;
public T get(int i){...};
public void add(T para){...};
.....
}
定义泛型类/方法/接口
泛型类就是具有一个或者多个类型变量的类。它的定义较为简单,参考上面的例子,使用一对尖括号(<>)将类型变量 T 括起来放在类名的后面。如果有多个,还可以使用K,V,U,S等,中间使用逗号隔开。
集合类和集合接口
Java的类库中有这么一些类,可以帮助我没在程序设计中实现传统的数据结构,比如链表,堆栈,队列,散列表等等。数据结构是程序设计中必学的课程,也有许多相关的书籍介绍和讲解使用方法,此处不再赘述。
此外,在学习数据结构时,一般会穿插讲解算法,对于一般人来说,算法只需要了解几种常见的排序算法和查找算法即可,在了解的基础上懂得如何使用,以及理解实现过程便可,并不需要在深入了解算法。
Java中的多线程
多线程基础介绍部分,笔者之前就有写过博客,详见:Java多线程
内容有错误之处,欢迎指出。