JAVA基础复习专栏

java程序运行机制:源文件(.java)→(编译器)→字节码文件(.class)→(解释器)→机器码

JDK JRE JVM
JDK (包含JRE,以及增加编译器和调试器等用于程序开发的文件)
JRE (Java虚拟机、库函数、运行Java应用程序所必须的文件)
JVM 即java虚拟机(指令集、寄存器集、结构栈、垃圾收集堆、内存区域),负责将Java字节码解释运行

注释:
单行注释 “//”
多行注释 以“/”开头以“/”结尾
文档注释 以“/**”开头以“*/”结尾

数据类型和运算符
标识符:必须以字母、下划线_、美元符号 开 头 。 其 它 部 分 可 以 是 字 母 、 下 划 线 “ ” 、 美 元 符 “ 开头。其它部分可以是字母、下划线“_”、美元符“ 线”和数字的任意组合。

变量:局部变量、成员变量(也称为实例变量)和静态变量。

常量:利用关键字final来定义一个常量。 常量一旦被初始化后不能再更改其值。

数据类型:8种基本数据类型 3种引用类型(引用数据类型的大小统一为4个字节,记录的是其引用对象的地址!)

整型变量/常量

常量的四种表示形式:
十进制整数,如:99, -500, 0
八进制整数,要求以 0 开头,如:015
十六进制数,要求 0x 或 0X 开头,如:0x15
二进制数,要求0b或0B开头,如:0b01110011
Java语言的整型常数默认为int型,声明long型常量可以后加‘ l ’或‘ L ’ 。

浮点型变量/常量

浮点型常量默认类型是double。
Java浮点类型常量有两种表示形式:
十进制数形式,例如:3.14 314.0 0.314
科学记数法形式,如314e2 314E2 314E-2

字符型变量/常量
char 类型用来表示在Unicode编码表中的字符。Unicode编码被设计用来处理各种语言的文字,它占2个字节,可允许有65536个字符。
转义字符:(前缀为u表示Unicode)

boolean类型变量/常量
boolean类型有两个常量值,true和false,在内存中占一位(不是一个字节),不可以使用 0 或非 0 的整数替代 true 和 false
一般:if ( flag )或者if ( !flag) 最好不要: if ( flag == true )

运算符

算术运算符
整数运算:
  1. 如果两个操作数有一个为Long, 则结果也为long。
  2. 没有long时,结果为int。即使操作数全为short,byte,结果也是int。
浮点运算:
  3. 如果两个操作数有一个为double,则结果为double。
  4. 只有两个操作数都是float,则结果才为float。
取模运算:
  1.其操作数可以为浮点数,一般使用整数,结果是“余数”,“余数”符号和左边操作数相同,如:7%3=1,-7%3=-1,7%-3=1。

关系运算符
=是赋值运算符,而真正的判断两个操作数是否相等的运算符是==。
==、!= 是所有(基本和引用)数据类型都可以使用

、>=、 <、 <= 仅针对数值类型(byte/short/int/long, float/double。以及char)

逻辑运算符

短路与和短路或采用短路的方式。从左到右计算,如果只通过运算符左边的操作数就能够确定该逻辑表达式的值,则不会继续计算运算符右边的操作数,提高效率。

位运算符

  1. &和|既是逻辑运算符,也是位运算符。如果两侧操作数都是boolean类型,就作为逻辑运算符。如果两侧的操作数是整数类型,就是位运算符。
  2. 不要把“^”当做数学运算“乘方”,是“位的异或”操作。

字符串连接符
“+”运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。

运算符优先级的问题

不需要去刻意的记这些优先级,表达式里面优先使用小括号来组织!!
逻辑与、逻辑或、逻辑非的优先级一定要熟悉!(逻辑非>逻辑与>逻辑或)。

自动类型转换
自动类型转换指的是容量小的数据类型可以自动转换为容量大的数据类型。实线表示无数据丢失的自动类型转换,虚线表示在转换时可能会有精度的损失。

强制类型转换:又被称为造型,用于显式的转换一个数值的类型。在有可能丢失信息的情况下进行的转换是通过造型来完成的,但可能造成精度降低或溢出。
语法格式:(type)var
注意:不能在布尔类型和任何数值类型之间做强制类型转换

控制语句
选择结构:if单选择结构 if-else双选择结构 if-else if-else多选择结构 switch结构
if单选择结构 如果if语句不写{},则只能作用于后面的第一条语句。
switch多选择结构 switch语句中case标签在JDK1.5之前必须是整数(long类型除外)或者枚举,不能是字符串,在JDK1.7之后允许使用字符串(String)。
当布尔表达式是等值判断的情况,可以使用if-else if-else多选择结构或者switch结构,如果布尔表达式区间判断的情况,则只能使用if-else if-else多选择结构。

循环结构:当型(while与for循环) 直到型(do-while循环)

语句块:语句块可以使用外部的变量,而外部不能使用语句块中定义的变量,因为语句块中定义的变量作用域只限于语句块。

方法:面向过程中,函数是最基本单位;面向对象中,整个程序的基本单位是类
注意:Java中进行方法调用中传递参数时,遵循值传递的原则(传递的都是数据的副本);
基本类型传递的是该数据值的copy值;
引用类型传递的是该对象引用的copy值,但指向的是同一个对象。
方法的重载:
构成方法重载的条件:
1.不同的含义:形参类型、形参个数、形参顺序不同
2.只有返回值不同不构成方法的重载
3.只有形参的名称不同,不构成方法的重载

递归结构包括两个部分:
1.递归头。解答:什么时候不调用自身方法。如果没有头,将陷入死循环,也就是递归的结束条件。
2.递归体。解答:什么时候需要调用自身方法。

对象的本质:
1.也是一种数据结构(对数据的管理模式),将数据和数据的行为放到了一起。
2.在内存上,对象就是一个内存块,存放了相关的数据集合!
3.对象的本质就一种数据的组织方式!

类的定义:
每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
对于一个类来说,一般有三种常见的成员:属性field、方法method、构造器constructor。这三种成员都可以定义零个或多个。
成员变量若未初始化,java默认初始化:

内存分析:
Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area。
栈的特点如下:
  1. 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)
  2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
  3. 栈属于线程私有,不能实现线程间的共享!
  4. 栈的存储特性是“先进后出,后进先出”
  5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!
堆的特点如下:
  1. 堆用于存储创建好的对象和数组(数组也是对象)
  2. JVM只有一个堆,被所有线程共享
  3. 堆是一个不连续的内存空间,分配灵活,速度慢!
方法区(又叫静态区)特点如下:
  1. JVM只有一个方法区,被所有线程共享!
  2. 方法区实际也是堆,只是用于存储类、常量相关的信息!
  3. 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量、字符串常量等)

垃圾回收原理和算法:
内存管理
  Java的内存管理很大程度指的就是对象的管理,其中包括对象空间的分配和释放。
  对象空间的分配:使用new关键字创建对象即可
  对象空间的释放:将对象赋值null即可。垃圾回收器将负责回收所有”不可达”对象的内存空间。
垃圾回收过程
  任何一种垃圾回收算法一般要做两件基本事情:
  1. 发现无用的对象
  2. 回收无用对象占用的内存空间。
  垃圾回收机制保证可以将“无用的对象”进行回收。无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。
垃圾回收相关算法
  1. 引用计数法
  堆中每个对象都有一个引用计数。被引用一次,计数加1. 被引用变量值变为null,则计数减1,直到计数为0,则表示变成无用对象。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。
2. 引用可达法(根搜索算法)
  程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

通用的分代垃圾回收机制;
不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、持久代。JVM将堆内存划分为 Eden、Survivor 和 Tenured/Old 空间。
  1. 年轻代
  所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。
  2. 年老代
  在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。
  3. 持久代
  用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。

Minor GC:
  用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)
Major GC:
  用于清理老年代区域。
Full GC:
  用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
垃圾回收过程:
1、新创建的对象,绝大多数都会存储在Eden中,
2、当Eden满了(达到一定比例)不能创建新对象,则触发垃圾回收(GC),将无用对象清理掉,然后剩余对象复制到某个Survivor中,如S1,同时清空Eden区
3、当Eden区再次满了,会将S1中的不能清空的对象存到另外一个Survivor中,如S2,同时将Eden区中的不能清空的对象,也复制到S1中,保证Eden和S1,均被清空。
4、重复多次(默认15次)Survivor中没有被清理的对象,则会复制到老年代Old(Tenured)区中,
5、当Old区满了,则会触发一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minorGC)

JVM调优和Full GC:
在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
  1.年老代(Tenured)被写满
  2.持久代(Perm)被写满
  3.System.gc()被显式调用(程序建议GC启动,不是调用GC)
  4.上一次GC之后Heap的各域分配策略动态变化

开发中容易造成内存泄露的操作:
1 创建大量无用对象
2 静态集合类的使用
像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放。
3 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭
IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。
4 监听器的使用
释放对象时,没有删除相应的监听器。
注意:

  1. 程序员无权调用垃圾回收器。
  2. 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
  3. finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。

Static关键字
static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念。
static变量前可以有private修饰,private是访问权限限定,static表示不要实例化就可以使用。static前面加上其它访问权限关键字的效果也以此类推。
因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。 因为实例成员与特定的对象关联。
核心要点:static修饰的成员变量和方法,从属于类。普通变量和方法从属于对象的。

静态初始化块
静态初始化块,用于类的初始化操作!在静态初始化块中不能直接访问非static成员。
静态初始化块执行顺序:
  1. 上溯到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到我们的类的静态初始化块为止。
  2. 构造方法执行顺序和上面顺序一样!!

参数传值机制:
基本数据类型参数的传值:
传递的是值的副本。 副本改变不会影响原件。
引用类型参数的传值:
  传递的是值的副本。但是引用类型指的是“对象的地址”。因此,副本和原参数都指向了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变”。

JDK中的主要包:

导入类import:Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。

静态导入:静态导入(static import)是在JDK1.5新增加的功能,其作用是用于导入指定类的静态属性,这样我们可以直接使用静态属性。

instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。

继承使用要点:
Java中类没有多继承,接口有多继承。
子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object。

方法的重写是实现多态的必要条件

和equals方法:
”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。
Object类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。
Object 的 equals 方法默认就是比较两个对象的hashcode,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals方法。
JDK提供的一些类,如String、Date、包装类等,重写了Object的equals方法,调用这些类的equals方法, x.equals (y) ,当x和y所引用的对象是同一类对象且属性内容相等时(并不一定是相同对象),返回 true 否则返回 false。

super关键字:
super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。
使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。
若是构造方法的第一行代码没有显式的调用super(…)或者this(…);那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略。

继承树追溯:
属性/方法查找顺序:(比如:查找变量h)

  1. 查找当前类中有没有属性h
  2. 依次上溯每个父类,查看每个父类中是否有h,直到Object
  3. 如果没找到,则出现编译错误。
  4. 上面步骤,只要找到h变量,则这个过程终止。
    构造方法调用顺序:
    构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
    注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。

封装优点:高内聚低耦合,提高代码安全性和复用性。

封装的实现—使用访问控制符

  1. private 表示私有,只有自己类能访问
  2. default表示没有修饰符修饰,只有同一个包的类能访问
  3. protected表示可以被同一个包的类以及其他包中的子类访问
  4. public表示可以被该项目的所有包中的所有类访问
    类的属性的处理:
  5. 一般使用private访问权限。
  6. 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。

对象的转型:
父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!

final关键字:

  1. 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
  2. 修饰方法:该方法不可被子类重写。但是可以被重载!
  3. 修饰类: 修饰的类不能被继承。比如:Math、String等。

抽象方法和抽象类:
抽象方法
使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。
抽象类
包含抽象方法的类就是抽象类。通过abstract方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。

接口和抽象类的区别:
抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。
接口和实现类不是父子关系,是实现规则的关系。

定义和使用接口:(面向接口编程)
声明格式:
[访问修饰符] interface 接口名 [extends 父接口1,父接口2…] {
常量定义;
方法定义;
}
定义接口的详细说明:
1. 访问修饰符:只能是public或默认。
2. 接口名:和类名采用相同命名机制。
3. extends:接口可以多继承。
4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
5. 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。
要点
1. 子类通过implements来实现接口中的规范。
2. 接口不能创建实例,但是可用于声明引用变量类型。
3. 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
4. JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
5. JDK1.8后,接口中包含普通的静态方法。

内部类
把一个类放在另一个类的内部定义,称为内部类(innerclasses)。
内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。
注意
内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。
内部类的作用:
1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。
3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。
内部类的使用场合:
1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。
2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。
成员内部类(非静态内部类、静态内部类)、匿名内部类、局部内部类:https://www.sxt.cn/Java_jQuery_in_action/five-classification.html

String:
String类又称作不可变字符序列。
Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。
当"+"运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。

常量池分了以下三种:

  1. 全局字符串常量池(String Pool)
    全局字符串常量池中存放的内容是在类加载完成后存到String Pool中的,在每个VM中只有一份,存放的是字符串常量的引用值(在堆中生成字符串对象实例)。
  2. class文件常量池(Class Constant Pool)
    class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量(文本字符串、final常量等)和符号引用。
  3. 运行时常量池(Runtime Constant Pool)
    运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

String类常用的方法:

字符串相等的判断:

  1. equals方法用来检测两个字符串内容是否相等。如果字符串s和t内容相等,则s.equals(t)返回true,否则返回false。
  2. 要测试两个字符串除了大小写区别外是否是相等的,需要使用equalsIgnoreCase方法。
  3. 判断字符串是否相等不要使用"=="。

开闭原则(Open-Closed Principle)就是让设计的系统对扩展开放,对修改封闭。
对扩展开放:
就是指,应对需求变化要灵活。 要增加新功能时,不需要修改已有的代码,增加新代码即可。
对修改关闭:
就是指,核心部分经过精心设计后,不再因为需求变化而改变。
在实际开发中,我们无法完全做到,但应尽量遵守开闭原则。

模板方法模式和回调机制:
模板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。
其实在Java开发中,还有另外一个方法可以实现同样的功能,那就是Java回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类中调用B类中的C方法,然后B类中的C方法中反过来调用A类中的D方法,那么D这个方法就叫回调方法。

组合模式:组合模式是将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

异常机制
Java是采用面向对象的方式来处理异常的。处理过程:

  1. 抛出异常:在执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止当前执行路径,并把异常对象提交给JRE。
  2. 捕获异常:JRE得到该异常后,寻找相应的代码来处理该异常。JRE在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。
    异常分类:

Error是程序无法处理的错误,表示运行应用程序中较严重问题。
Exception是程序本身能够处理的异常,如:空指针异常(NullPointerException)、数组下标越界异常(ArrayIndexOutOfBoundsException)、类型转换异常(ClassCastException)、算术异常(ArithmeticException)等。
Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。 通常Java的异常可分为:
1. RuntimeException 运行时异常
2. CheckedException 已检查异常
RuntimeException运行时异常:这类异常通常是由编程错误导致的,所以在编写程序时,并不要求必须使用异常处理机制来处理这类异常,经常需要通过增加“逻辑处理来避免这些异常”。
所有不是RuntimeException的异常,统称为Checked Exception,又被称为“已检查异常”,如IOException、SQLException等以及用户自定义的Exception异常。 这类异常在编译时就必须做出处理,否则无法通过编译。

异常的处理方式有两种:使用“try/catch”捕获异常、使用“throws”声明异常。
try-catch-finally。用try来执行一段程序,如果出现异常,系统抛出一个异常,可以通过它的类型来捕捉(catch)并处理它,最后一步是通过finally语句为异常处理提供一个统一的出口,finally所指定的代码都要被执行(catch语句可有多条;finally语句最多只能有一条,根据自己的需要可有可无)。
  1. try:
    try语句指定了一段代码,该段代码就是异常捕获并处理的范围。在执行过程中,当任意一条语句产生异常时,就会跳过该条语句中后面的代码。代码中可能会产生并抛出一种或几种类型的异常对象,它后面的catch语句要分别对这些异常做相应的处理。一个try语句必须带有至少一个catch语句块或一个finally语句块 。
    注意事项
    当异常处理的代码执行结束以后,不会回到try语句去执行尚未执行的代码。
  2. catch:
    每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
    常用方法,这些方法均继承自Throwable类 。
    toString ()方法,显示异常的类名和产生异常的原因
    getMessage()方法,只显示产生异常的原因,但不显示类名。
    printStackTrace()方法,用来跟踪异常事件发生时堆栈的内容。
    catch捕获异常时的捕获顺序:
    如果异常类之间有继承关系,在顺序安排上需注意。越是顶层的类,越放在下面,再不然就直接把多余的catch省略掉。 也就是先捕获子类异常再捕获父类异常。
    注意事项
    1. 即使try和catch块中存在return语句,finally语句也会执行。是在执行完finally语句后再通过return退出。
    2. finally语句块只有一种情况是不会执行的,那就是在执行finally之前遇到了System.exit(0)结束程序运行。
      声明异常(throws子句):
      如果一个方法中可能产生某种异常,但是并不能确定如何处理这种异常,则应根据异常规范在方法的首部声明该方法可能抛出的异常。如果一个方法抛出多个已检查异常,就必须在方法的首部列出所有的异常,之间以逗号隔开。
      注意事项:方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。

自定义异常:在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
自定义异常类只需从Exception类或者它的子类派生一个子类即可。
自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类。
习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。
使用异常机制的建议:
1.要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
2.处理异常不可以代替简单测试—只在异常情况下使用异常机制。
3.不要进行小粒度的异常处理—应该将整个任务包装在一个try语句块中。
4.异常往往在高层处理。

增强for循环for-each是JDK1.5新增加的功能,专门用于读取数组或集合中所有的元素,即对数组进行遍历。

  1. for-each增强for循环在遍历数组过程中不能修改数组中某元素的值。
  2. for-each仅适用于遍历,不涉及有关索引(下标)的操作。

System类里也包含了一个static void arraycopy(object src,int srcpos,object dest, int destpos,int length)方法,该方法可以将src数组里的元素值赋给dest数组的元素,其中srcpos指定从src数组的第几个元素开始赋值,length参数指定将src数组的多少个元素赋给dest数组的元素。

常用类
包装类:八个和基本数据类型对应的类统称为包装类(Wrapper Class),包装类均位于java.lang包。

  在这八个类名中,除了Integer和Character类以外,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写而已。
  在这八个类中,除了Character和Boolean以外,其他的都是“数字型”,“数字型”都是java.lang.Number的子类。Number类是抽象类,因此它的抽象方法,所有子类都需要提供实现。Number类提供了抽象方法:intValue()、longValue()、floatValue()、doubleValue(),意味着所有的“数字型”包装类都可以互相转型。

void testInteger() {
// 基本类型转化成Integer对象
Integer int1 = new Integer(10);
Integer int2 = Integer.valueOf(20); // 官方推荐这种写法
// Integer对象转化成int
int a = int1.intValue();
// 字符串转化成Integer对象
Integer int3 = Integer.parseInt(“334”);
Integer int4 = new Integer(“999”);
// Integer对象转化成字符串
String str1 = int3.toString();
// 一些常见int类型相关的常量
System.out.println(“int能表示的最大整数:” + Integer.MAX_VALUE);
}

自动装箱和拆箱:将基本数据类型和包装类之间进行自动的互相转换。
自动装箱过程是通过调用包装类的valueOf()方法实现的,而自动拆箱过程是通过调用包装类的 xxxValue()方法实现的(xxx代表对应的基本数据类型,如intValue()、doubleValue()等)。
自动装箱与拆箱的功能事实上是编译器来帮的忙,编译器在编译时依据您所编写的语法,决定是否进行装箱或拆箱动作。
包装类的缓存问题: 整型、char类型所对应的包装类,在自动装箱时,对于-128~127之间的值会进行缓存处理,其目的是提高效率。
原理:如果数据在-128~127这个区间,那么在类加载时就已经为该区间的每个数值创建了对象,并将这256个对象存放到一个名为cache的数组中。每当自动装箱过程发生时(或者手动调用valueOf()时),就会先判断数据是否在该区间,如果在则直接获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建对象。

String类:
String 类对象代表不可变的Unicode字符序列,因此我们可以将String对象称为“不可变对象”。
StringBuffer和StringBuilder非常类似,均代表可变的字符序列。
区别:
1. StringBuffer JDK1.0版本提供的类,线程安全,做线程同步检查, 效率较低。
2. StringBuilder JDK1.5版本提供的类,线程不安全,不做线程同步检查,因此效率较高。 建议采用该类。
String使用的陷阱:
String一经初始化后,就不会再改变其内容了。对String字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。比如:
String s =“a”; 创建了一个字符串
s = s+“b”; 实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串s+“b”(也就是"ab")。 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。

时间处理相关类:
1970 年 1 月 1 日 00:00:00定为基准时间,用long类型的变量来表示时间,从基准时间往前几亿年,往后几亿年都能表示。如果想获得现在时刻的“时刻数值”,可以使用:long now = System.currentTimeMillis();
这个“时刻数值”是所有时间类的核心值,年月日都是根据这个“数值”计算出来的。

Date时间类(java.util.Date):(可以理解为计秒数)
在标准Java类库中包含一个Date类。它的对象表示一个特定的瞬间,精确到毫秒。
1. Date() 分配一个Date对象,并初始化此对象为系统当前的日期和时间,可以精确到毫秒。
2. Date(long date) 分配 Date 对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即 1970 年 1 月 1 日 00:00:00 GMT)以来的指定毫秒数。
3. boolean after(Date when) 测试此日期是否在指定日期之后。
4. booleanbefore(Date when) 测试此日期是否在指定日期之前。
5. boolean equals(Object obj) 比较两个日期的相等性。
6. long getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
7. String toString() 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天

DateFormat类的作用
把时间对象转化成指定格式的字符串。反之,把指定格式的字符串转化成时间对象。
DateFormat是一个抽象类,一般使用它的的子类SimpleDateFormat类来实现。

Calendar日历类:
Calendar 类是一个抽象类,为我们提供了关于日期计算的相关功能,比如:年、月、日、时、分、秒的展示和计算。
GregorianCalendar 是 Calendar 的一个具体子类,提供了世界上大多数国家/地区使用的标准日历系统。
注意:月份的表示,一月是0,二月是1,以此类推,12月是11。 因为大多数人习惯于使用单词而不是使用数字来表示月份,这样程序也许更易读,父类Calendar使用常量来表示月份:JANUARY、FEBRUARY等等。

Math类的常用方法:
1. abs 绝对值
2. acos,asin,atan,cos,sin,tan 三角函数
3. sqrt 平方根
4. pow(double a, double b) a的b次幂
5. max(double a, double b) 取大值
6. min(double a, double b) 取小值
7. ceil(double a) 大于a的最小整数
8. floor(double a) 小于a的最大整数
9. random() 返回 0.0 到 1.0 的随机数
10. long round(double a) double型的数据a转换为long型(四舍五入)
11. toDegrees(double angrad) 弧度->角度
12. toRadians(double angdeg) 角度->弧度

File类:
java.io.File类:代表文件和目录。 在开发中,读取文件、生成文件、删除文件、修改文件的属性时经常会用到本类。
File类的常见构造方法:public File(String pathname)。以pathname为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储

枚举类型的定义包括枚举声明和枚举体。格式如下:
enum 枚举名 {
枚举体(常量列表)
}
所有的枚举类型隐性地继承自 java.lang.Enum。枚举实质上还是类!而每个被枚举的成员实质就是一个枚举类型的实例,他们默认都是public static final修饰的。可以直接通过枚举类型名使用它们。

容器(List、Set、Map)
泛型:(基于数组并不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活、容量随时可扩的容器来装载我们的对象。)
泛型是JDK1.5以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可读性和安全性。
泛型的本质就是“数据类型的参数化”。 我们可以把“泛型”理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。
声明处增加泛型列表,如:<T,E,V>。此处,字符可以是任何标识符,一般采用这3个字母。
容器相关类都定义了泛型,我们在开发和工作中,在使用容器类时都要使用泛型。这样,在容器的存储数据、读取数据时都避免了大量的类型判断,非常便捷。
Collection、List、Set、Map、Iterator接口都定义了泛型。因此,在使用这些接口及其实现类时,都要使用泛型。只是强烈建议使用泛型。事实上,不使用编译器也不会报错!

集合:

Collection接口的两个子接口是List、Set接口。

List是有序、可重复的容器。 List接口常用的实现类有3个:ArrayList、LinkedList和Vector。

ArrayList:ArrayList底层是用数组实现的存储。 特点:查询效率高,增删效率低,线程不安全。我们一般使用它。
ArrayList底层使用Object数组来存储元素数据。
ArrayList可以存放任意数量的对象,长度不受限制,本质上就是通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。
LinkedList:LinkedList底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。
Vector:Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。
如何选用ArrayList、LinkedList、Vector?
1. 需要线程安全时,用Vector。
2. 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。
3. 不存在线程安全问题时,增加或删除元素较多用LinkedList。

Set:
Set接口继承自Collection,Set接口中没有新增方法,方法和Collection保持完全一致。List的方法,在Set中仍然适用。
Set容器特点:无序、不可重复。无序指Set中的元素没有索引,我们只能遍历查找;不可重复指不允许加入重复的元素。更确切地讲,新元素如果和Set中某个元素通过equals()方法对比为true,则不能加入;甚至,Set中也只能放入一个null元素,不能多个。
Set常用的实现类有:HashSet、TreeSet等,一般使用HashSet。
HashSet底层实现:
HashSet是采用哈希算法实现,底层实际是用HashMap实现的(HashSet本质就是一个简化版的HashMap),因此,查询效率和增删效率都比较高。

  里面有个map属性,这就是HashSet的核心秘密。再看add()方法,发现增加一个元素说白了就是在map中增加一个键值对,键对象就是这个元素,值对象是名为PRESENT的Object对象。说白了,就是“往set中加入元素,本质就是把这个元素作为key加入到了内部的map中”。
  由于map中key都是不可重复的,因此,Set天然具有“不可重复”的特性。

TreeSet的使用和底层实现:
TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。 TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
(1) 由于是二叉树,需要对元素做内部排序。 如果要放入TreeSet中的类没有实现Comparable接口,则会抛出异常:java.lang.ClassCastException。
(2) TreeSet中不能放入null元素。

Map:
Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。
Map 接口的实现类有HashMap、TreeMap、HashTable、Properties等。

HashMap和HashTable:
HashMap采用哈希算法实现,是Map接口最常用的实现类。 由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。 HashMap在查找、删除、修改方面都有非常高的效率。
HashMap底层实现采用了哈希表,这是一种非常重要的数据结构。 哈希表的本质就是“数组+链表”。
HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。

(1) 获得key对象的hashcode
(2) 根据hashcode计算出hash值(要求在[0, 数组长度-1]区间)
i. 一种极端简单和低下的算法是: hash值 = hashcode/hashcode;
ii. 一种简单和常用的算法是(相除取余算法):hash值 = hashcode%数组长度
由于使用了“除法”,效率低下。JDK后来改进了算法。首先约定数组长度必须为2的整数幂,这样采用位运算即可实现取余的效果:hash值 = hashcode&(数组长度-1)。
(3) 生成Entry对象
(4) 将Entry对象放到table数组中
当添加一个元素(key-value)时,首先计算key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的Hash值是相同的,所以说数组存放的是链表。 JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

取数据过程get(key):
我们需要通过key对象获得“键值对”对象,进而返回value对象。明白了存储数据过程,取数据就比较简单了,参见以下步骤:
(1) 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。
(2) 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。
(3) 返回equals()为true的节点对象的value对象。
明白了存取数据的过程,我们再来看一下hashcode()和equals方法的关系:
Java中规定,两个内容相同(equals()为true)的对象必须具有相等的hashCode。因为如果equals()为true而两个对象的hashcode不同;那在整个存储过程中就发生了悖论。

扩容问题:
HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组 length), 就重新调整数组大小变为原来2倍大小。
扩容很耗时。扩容的本质是定义新的更大的数组,并将旧数组内容挨个拷贝到新数组中。

JDK8将链表在大于8情况下变为红黑二叉树:
JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

其中的Entry[] table 就是HashMap的核心数组结构,我们也称之为“位桶数组”。
一个Entry对象存储了:
1. key:键对象 value:值对象
2. next:下一个节点
3. hash: 键对象的hash值

HashMap与HashTable的区别:
1. HashMap: 线程不安全,效率高。允许key或value为null。
2. HashTable: 线程安全,效率低。不允许key或value为null。

java有四类基本结构:集合、线性结构、树形结构、图状结构

二叉树和红黑二叉树:
排序二叉树特性:
(1) 左子树上所有节点的值均小于它的根节点的值。
(2) 右子树上所有节点的值均大于它的根节点的值。
平衡二叉树(AVL):
为了避免出现上述一边倒的存储,科学家提出了“平衡二叉树”。
在平衡二叉树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除节点可能需要通过一次或多次树旋转来重新平衡这个树。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。
平衡二叉树追求绝对平衡,实现起来比较麻烦,每次插入新节点需要做的旋转操作次数不能预知。
红黑二叉树:
红黑二叉树(简称:红黑树),它首先是一棵二叉树,同时也是一棵自平衡的排序二叉树。
红黑树在原有的排序二叉树增加了如下几个要求:
1. 每个节点要么是红色,要么是黑色。
2. 根节点永远是黑色的。
3. 所有的叶节点都是空节点(即 null),并且是黑色的。
4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。
红黑树是一个更高效的检索二叉树,JDK 提供的集合类 TreeMap、TreeSet 本身就是一个红黑树的实现。

   红黑树的基本操作:插入、删除、左旋、右旋、着色。 每插入或者删除一个节点,可能会导致树不在符合红黑树的特征,需要进行修复,进行 “左旋、右旋、着色”操作,使树继续保持红黑树的特性。

TreeMap:TreeMap是红黑二叉树的典型实现。

里面存储了本身数据、左节点、右节点、父节点、以及节点颜色。 TreeMap的put()/remove()方法大量使用了红黑树的理论。
TreeMap和HashMap实现了同样的接口Map,因此,用法对于调用者来说没有区别。HashMap效率高于TreeMap;在需要排序的Map时才选用TreeMap。

Collections工具类:
类 java.util.Collections 提供了对Set、List、Map进行排序、填充、查找元素的辅助方法。
1. void sort(List) //对List容器内的元素排序,排序的规则是按照升序进行排序。
2. void shuffle(List) //对List容器内的元素进行随机排列。
3. void reverse(List) //对List容器内的元素进行逆续排列 。
4. void fill(List, Object) //用一个特定的对象重写整个List容器。
5. int binarySearch(List, Object)//对于顺序的List容器,采用折半查找的方法查找特定对象。

IO技术
输入(Input)指的是:可以让程序从外部系统获得数据(核心含义是“读”,读取外部数据)。
输出(Output)指的是:程序输出数据给外部系统从而可以操作外部系统(核心含义是“写”,将数据写出到外部系统)。

流的概念细分:
按流的方向分类:
1. 输入流:数据流向是数据源到程序(以InputStream、Reader结尾的流)。
2. 输出流:数据流向是程序到目的地(以OutPutStream、Writer结尾的流)。
按处理的数据单元分类:
1. 字节流:以字节为单位获取数据,命名上以Stream结尾的流一般是字节流,如FileInputStream、FileOutputStream。
2. 字符流:以字符为单位获取数据,命名上以Reader/Writer结尾的流一般是字符流,如FileReader、FileWriter。
按处理对象不同分类:
1. 节点流:可以直接从数据源或目的地读写数据,如FileInputStream、FileReader、DataInputStream等。
2. 处理流:不直接连接到数据源或目的地,是”处理流的流”。通过对其他流的处理提高程序的性能,如BufferedInputStream、BufferedReader等。处理流也叫包装流。
节点流处于IO操作的第一线,所有操作必须通过它们进行;处理流可以对节点流进行包装,提高性能或提高程序的灵活性。

IO流体系:

总结:

  1. InputStream/OutputStream
    字节流的抽象类。
  2. Reader/Writer
    字符流的抽象类。
  3. FileInputStream/FileOutputStream
    节点流:以字节为单位直接操作“文件”。
  4. ByteArrayInputStream/ByteArrayOutputStream
    节点流:以字节为单位直接操作“字节数组对象”。
  5. ObjectInputStream/ObjectOutputStream
    处理流:以字节为单位直接操作“对象”。
  6. DataInputStream/DataOutputStream
    处理流:以字节为单位直接操作“基本数据类型与字符串类型”。
  7. FileReader/FileWriter
    节点流:以字符为单位直接操作“文本文件”(注意:只能读写文本文件)。
  8. BufferedReader/BufferedWriter
    处理流:将Reader/Writer对象进行包装,增加缓存功能,提高读写效率。
  9. BufferedInputStream/BufferedOutputStream
    处理流:将InputStream/OutputStream对象进行包装,增加缓存功能,提高 读写效率。
  10. InputStreamReader/OutputStreamWriter
    处理流:将字节流对象转化成字符流对象
  11. PrintStream
    处理流:将OutputStream进行包装,可以方便地输出字符,更加灵活。

四大IO抽象类:
InputStream
此抽象类是表示字节输入流的所有类的父类。InputSteam是一个抽象类,它不可以实例化。 数据的读取需要由它的子类来实现。根据节点的不同,它派生了不同的节点流子类 。
继承自InputSteam的流都是用于向程序中输入数据,且数据的单位为字节(8 bit)。
常用方法:
int read():读取一个字节的数据,并将字节的值作为int类型返回(0-255之间的一个值)。如果未读出字节则返回-1(返回值为-1表示读取结束)。
void close():关闭输入流对象,释放相关系统资源。
OutputStream
此抽象类是表示字节输出流的所有类的父类。输出流接收输出字节并将这些字节发送到某个目的地。
常用方法:
void write(int n):向目的地中写入一个字节。
void close():关闭输出流对象,释放相关系统资源。
Reader
Reader用于读取的字符流抽象类,数据单位为字符。
int read(): 读取一个字符的数据,并将字符的值作为int类型返回(0-65535之间的一个值,即Unicode值)。如果未读出字符则返回-1(返回值为-1表示读取结束)。
void close() : 关闭流对象,释放相关系统资源。
Writer
Writer用于写入的字符流抽象类,数据单位为字符。
void write(int n): 向输出流中写入一个字符。
void close() : 关闭输出流对象,释放相关系统资源。

文件字节流:
FileInputStream通过字节的方式读取文件,适合读取所有类型的文件(图像、视频、文本文件等)。Java也提供了FileReader专门读取文本文件。
FileOutputStream 通过字节的方式写数据到文件中,适合所有类型的文件。Java也提供了FileWriter专门写入文本文件。
文件字符流:(FileReader,FileWriter)
字节流不能很好的处理Unicode字符,经常会出现“乱码”现象。所以,我们处理文本文件,一般可以使用文件字符流,它以字符为单位进行操作。
缓冲字节流:
Java缓冲流本身并不具有IO流的读取与写入功能,只是在别的流(节点流或其他处理流)上加上缓冲功能提高效率,就像是把别的流包装起来一样,因此缓冲流是一种处理流(包装流)。
当对文件或者其他数据源进行频繁的读写操作时,效率比较低,这时如果使用缓冲流就能够更高效的读写信息。因为缓冲流是先将数据缓存起来,然后当缓存区存满后或者手动刷新时再一次性的读取到程序或写入目的地。在IO操作时记得加上缓冲流来提升性能。
BufferedInputStream和BufferedOutputStream这两个流是缓冲字节流,通过内部缓存数组来提高操作流的效率。
缓冲字符流:
BufferedReader/BufferedWriter增加了缓存机制,大大提高了读写文本文件的效率,同时,提供了更方便的按行读取的方法:readLine(); 处理文本时,我们一般可以使用缓冲字符流。
注意:
1. readLine()方法是BufferedReader特有的方法,可以对文本文件进行更加方便的读取操作。
2. 写入一行后要记得使用newLine()方法换行。
字节数组流:
ByteArrayInputStream和ByteArrayOutputStream经常用在需要流和数组之间转化的情况!
数据流:
数据流将“基本数据类型与字符串类型”作为数据源,从而允许程序以与机器无关的方式从底层输入输出流中操作Java基本数据类型与字符串类型。
DataInputStream和DataOutputStream提供了可以存取与机器无关的所有Java基础类型数据(如:int、double、String等)的方法。
DataInputStream和DataOutputStream是处理流,可以对其他节点流或处理流进行包装,增加一些更灵活、更高效的功能。
注意:使用数据流时,读取的顺序一定要与写入的顺序一致,否则不能正确读取数据。
对象流:
数据流只能实现对基本数据类型和字符串类型的读写,并不能读取对象(字符串除外),如果要对某个对象进行读写操作,我们需要学习一对新的处理流: ObjectInputStream/ObjectOutputStream。ObjectInputStream/ObjectOutputStream是以“对象”为数据源,但是必须将传输的对象进行序列化与反序列化操作。
注意:
1. 对象流不仅可以读写对象,还可以读写基本数据类型。
2. 使用对象流读写对象时,该对象必须序列化与反序列化。
3. 系统提供的类(如Date等)已经实现了序列化接口,自定义类必须手动实现序列化接口。
转换流:
InputStreamReader/OutputStreamWriter用来实现将字节流转化成字符流。比如,如下场景:
System.in是字节流对象,代表键盘的输入,如果我们想按行接收用户的输入时,就必须用到缓冲字符流BufferedReader特有的方法readLine(),但是经过观察会发现在创建BufferedReader的构造方法的参数必须是一个Reader对象,这时候我们的转换流InputStreamReader就派上用场了。
而System.out也是字节流对象,代表输出到显示器,按行读取用户的输入后,并且要将读取的一行字符串直接显示到控制台,就需要用到字符流的write(String str)方法,所以我们要使用OutputStreamWriter将字节流转化为字符流。
装饰器模式:IO流体系中大量使用了装饰器模式,让流具有更强的功能、更强的灵活性。比如BufferedInputStream装饰了原有的FileInputStream,让普通的FileInputStream也具备了缓存功能,提高了效率。
Apache IOUtils和FileUtils的使用:
JDK中提供的文件操作相关的类,但是功能都非常基础,进行复杂操作时需要做大量编程工作。实际开发中,往往需要自己动手编写相关的代码,尤其在遍历目录文件时,经常用到递归,非常繁琐。 Apache-commons工具包中提供了IOUtils/FileUtils,可以让我们非常方便的对文件和目录进行操作。

序列化和反序列化:
当两个进程远程通信时,彼此可以发送各种类型的数据。 无论是何种类型的数据,都会以二进制序列的形式在网络上传送。
对象序列化的作用有如下两种:
1. 持久化: 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中,比如:休眠的实现。以后服务器session管理,hibernate将对象持久化实现。
2. 网络通信:在网络上传送对象的字节序列。比如:服务器之间的数据通信、对象传递。
序列化涉及的类和接口:
ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
只有实现了Serializable接口的类的对象才能被序列化。 Serializable接口是一个空接口,只起到标记作用。
注意:
1. static属性不参与序列化。
2. 对象中的某些属性如果不想被序列化,不能使用static,而是使用transient修饰。
3. 为了防止读和写的序列化ID不一致,一般指定一个固定的序列化ID。

多线程技术
进程:执行中的程序叫做进程(Process),是一个动态的概念。进程是程序的一次动态执行过程, 占用特定的地址空间。每个进程由3部分组成:cpu、data、code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西,这样的缺点是:浪费内存,cpu的负担较重。

线程和进程的区别:
1. 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。
2. 线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
3. 线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。
4. 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
进程与程序的区别:
一个进程肯定与一个程序相对应,并且只有一个,但是一个程序可以有多个进程,或者一个进程都没有。进程是程序的一部分,程序运行的时候会产生进程。

通过继承Thread类实现多线程:
继承Thread类实现多线程的步骤:
1. 在Java中负责实现线程功能的类是java.lang.Thread 类。
2. 可以通过创建 Thread的实例来创建新的线程。
3. 每个线程都是通过某个特定的Thread对象所对应的方法run( )来完成其操作的,方法run( )称为线程体。
4. 通过调用Thread类的start()方法来启动一个线程。
缺点:如果类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。
通过Runnable接口实现多线程:
在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了Thread实现线程类的缺点,即在实现Runnable接口的同时还可以继承某个类。所以实现Runnable接口的方式要通用一些。

线程状态:(5个状态)

新生状态(New):
用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。
就绪状态(Runnable):
处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4中原因会导致线程进入就绪状态:
1. 新建线程:调用start()方法,进入就绪状态;
2. 阻塞线程:阻塞解除,进入就绪状态;
3. 运行线程:调用yield()方法,直接进入就绪状态;
4. 运行线程:JVM将CPU资源从本线程切换到其他线程。
运行状态(Running):
在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
阻塞状态(Blocked):
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:
1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
死亡状态(Terminated):
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。
当一个线程进入死亡状态以后,就不能再回到其它状态了。

终止线程的典型方式:终止线程通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。
暂停线程执行sleep/yield:暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:
1. sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
2. yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。
线程的联合join():线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。
获取线程基本信息的方法:

线程的优先级:(值越大,优先级越高)

  1. 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选。
  2. 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。
  3. 使用下列方法获得或设置线程对象的优先级。
    int getPriority();
    void setPriority(int newPriority);
    注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。

线程同步:线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
实现线程同步:synchronized 方法和 synchronized 块。
synchronized 方法:
通过在方法声明中加入 synchronized关键字来声明,语法如下:
public synchronized void accessVal(int newVal);
synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
synchronized块:
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。
synchronized 块:通过 synchronized关键字来声明synchronized 块,语法如下:
synchronized(syncObject)
  {
   //允许访问控制的代码
  }
只有获得syncObject的锁,才可以执行代码块

死锁及解决方案:
“死锁”指的是:多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。因此, 某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
死锁的解决方法:
死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。

线程并发协作(生产者/消费者模式):(重点在于构造缓冲区)
多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
生产者:指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
消费者:消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

缓冲区是实现并发的核心,缓冲区的设置有3个好处:
实现线程的并发协作:
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
解耦了生产者和消费者:
生产者不需要和消费者直接打交道。
解决忙闲不均,提高效率:
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。
线程并发协作总结:
线程并发协作(也叫线程通信),通常用于生产者/消费者模式,情景如下:
1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。
3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
4. 在生产者消费者问题中,仅有synchronized是不够的。
· synchronized可阻止并发更新同一个共享资源,实现了同步;
· synchronized不能用来实现不同线程之间的消息传递(通信)。
5. 那线程是通过哪些方法来进行消息传递(通信)的呢?见如下总结:

  6. 以上方法均是java.lang.Object类的方法;
  都只能在同步方法或者同步代码块中使用,否则会抛出异常。

任务定时调度:
通过Timer和Timetask,我们可以实现定时启动某个线程。
java.util.Timer:
在这种实现方式中,Timer类作用是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。
java.util.TimerTask:
TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。
在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
使用案例:
public class TestTimer {
public static void main(String[] args) {
Timer t1 = new Timer();//定义计时器;
MyTask task1 = new MyTask();//定义任务;
t1.schedule(task1,3000); //3秒后执行;
//t1.schedule(task1,5000,1000);//5秒以后每隔1秒执行一次!
//GregorianCalendar calendar1 = new GregorianCalendar(2010,0,5,14,36,57);
//t1.schedule(task1,calendar1.getTime()); //指定时间定时执行;
}
}
class MyTask extends TimerTask {//自定义线程类继承TimerTask类;
public void run() {
for(int i=0;i<10;i++){
System.out.println(“任务1:”+i);
}
}
}
实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask实现。

网络编程
OSI(Open System Interconnect,即开放系统互联)模型。OSI模型制定的七层标准模型,分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。

1.数据发送处理过程:
(1)应用层将数据交给传输层,传输层添加上TCP的控制信息(称为TCP头部),这个数据单元称为段(Segment),加入控制信息的过程称为封装。然后,将段交给网络层。
(2)网络层接收到段,再添加上IP头部,这个数据单元称为包(Packet)。然后,将包交给数据链路层。
(3) 数据链路层接收到包,再添加上MAC头部和尾部,这个数据单元称为帧(Frame)。然后,将帧交给物理层。
(4)物理层将接收到的数据转化为比特流,然后在网线中传送。
2.数据接收处理过程:
(1)物理层接收到比特流,经过处理后将数据交给数据链路层。
(2)数据链路层将接收到的数据转化为数据帧,再除去MAC头部和尾部,这个除去控制信息的过程称为解封,然后将包交给网络层。
(3)网络层接收到包,再除去IP头部,然后将段交给传输层。
(4)传输层接收到段,再除去TCP头部,然后将数据交给应用层。
从以上传输过程中,可以总结出以下规则:
(1)发送方数据处理的方式是从高层到底层,逐层进行数据封装。
(2)接收方数据处理的方式是从底层到高层,逐层进行数据解封装。

IP、端口、URL、套接字:
IP地址用来标识一台计算机,端口用来标识网络应用程序。
URL:在www上,每一信息资源都有统一且唯一的地址,该地址就叫URL(Uniform Resource Locator),它是www的统一资源定位符。URL由4部分组成:协议 、存放资源的主机域名、资源文件名和端口号。如果未指定该端口号,则使用协议默认的端口。
Socket实际是传输层供给应用层的编程接口。Socket就是应用层与传输层之间的桥梁。

Socket是传输层供给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类。

  1. TCP是面向连接的,传输数据安全,稳定,效率相对较低。
  2. UDP是面向无连接的,传输数据不安全,效率较高。
    TCP的三次握手:
    第一步,是请求端(客户端)发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。
    第二步,服务器在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示客户端的请求被接受,同时TCP序号被加一,ACK即确认(Acknowledgement)。
    第三步,客户端也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成。然后才开始通信的第二步:数据处理。
    UDP:基于UDP协议的通信方式,称为数据报通信方式。在这种方式中,每个数据发送单元被统一封装成数据报包的方式,发送方将数据报包发送到网络中,数据报包在网络中去寻找它的目的地。
    Java网络编程:Java为了可移植性,不允许直接调用操作系统,而是由java.net包来提供网络功能。Java虚拟机负责提供与操作系统的实际连接。

InetAddress:
作用:封装计算机的IP地址和DNS(没有端口信息)。注:DNS是Domain Name System,域名系统。
特点:这个类没有构造方法。如果要得到对象,只能通过静态方法:getLocalHost()、getByName()、 getAllByName()、 getAddress()、getHostName()。
InetSocketAddress:
作用:包含IP和端口信息,常用于Socket通信。此类实现 IP 套接字地址(IP 地址 + 端口号),不依赖任何协议。
URL类:(类 URL 代表一个统一资源定位符,它是指向互联网“资源”的指针。)
JDK中提供了URL类,该类的全名是java.net.URL,有了这样一个类,就可以使用它的各种方法来对URL对象进行分割、合并等处理。

基于TCP协议的Socket编程和通信:
在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。
“请求-响应”模式:
1. Socket类:发送TCP消息。
2. ServerSocket类:创建服务器。
在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样,客户端和服务器通过套接字所建立的连接使用输入输出流进行通信。
通过Socket的编程顺序:
1. 创建服务器ServerSocket,在创建时,定义ServerSocket的监听端口(在这个端口接收客户端发来的消息)。
2. ServerSocket调用accept()方法,使之处于阻塞状态。
3. 创建客户端Socket,并设置服务器的IP及端口。
4. 客户端发出连接请求,建立连接。
5. 分别取得服务器和客户端Socket的InputStream和OutputStream。
6. 利用Socket和ServerSocket进行数据传输。
7. 关闭流及Socket。
运行时,要先启动服务器端,再启动客户端,才能得到正常的运行效果。
但是,必须按照安排好的顺序,服务器和客户端一问一答!不够灵活!!可以使用多线程实现更加灵活的双向通讯:
服务器端:一个线程专门发送消息,一个线程专门接收消息。
客户端:一个线程专门发送消息,一个线程专门接收消息。

UDP通讯的实现:
DatagramSocket:用于发送或接收数据报包
当服务器要向客户端发送数据时,需要在服务器端产生一个DatagramSocket对象,在客户端产生一个DatagramSocket对象。服务器端的DatagramSocket将DatagramPacket发送到网络上,然后被客户端的DatagramSocket接收。
DatagramSocket有两种常用的构造函数。一种是无需任何参数的,常用于客户端;另一种需要指定端口,常用于服务器端。如下所示:
DatagramSocket() :构造数据报套接字并将其绑定到本地主机上任何可用的端口。
DatagramSocket(int port) :创建数据报套接字并将其绑定到本地主机上的指定端口。
常用方法:
Ø send(DatagramPacket p) :从此套接字发送数据报包。
Ø receive(DatagramPacket p) :从此套接字接收数据报包。
Ø close() :关闭此数据报套接字。
▪ DatagramPacket:数据容器(封包)的作用
此类表示数据报包。 数据报包用来实现封包的功能。

常用方法:
Ø DatagramPacket(byte[] buf, int length) :构造数据报包,用来接收长度为 length 的数据包。
Ø DatagramPacket(byte[] buf, int length, InetAddress address, int port) :构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
Ø getAddress() :获取发送或接收方计算机的IP地址,此数据报将要发往该机器或者是从该机器接收到的。
Ø getData() :获取发送或接收的数据。
Ø setData(byte[] buf) :设置发送的数据。
UDP通信编程基本步骤:
1. 创建客户端的DatagramSocket,创建时,定义客户端的监听端口。
2. 创建服务器端的DatagramSocket,创建时,定义服务器端的监听端口。
3. 在服务器端定义DatagramPacket对象,封装待发送的数据包。
4. 客户端将数据报包发送出去。
5. 服务器端接收数据报包。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值