一、Java概述
1.java编译产生与平台无关的字节码(*.class文件),再在JVM里面执行。
2.JVM是一个抽象的计算机,具有指令集并使用不同的存储区,负责执行指令,还要管理数据、内存和寄存器。
3.JVM细节:指令集、寄存器、类文件的格式、栈、垃圾回收堆、存储区。
4.只运行java程序可以只安装JRE,若要开发则要JDK
5.bin路径下的绝大部分命令都是包装了tools.jar文件里的工具类。
6.JDK1.5之后可以不用设置CLASSPATH环境变量。
7.main方法的 public 和 static可以交换位置。
8.垃圾回收相关:将对象引用设置为null将加快垃圾回收;可以通过System.gc()来建议系统进行垃圾回收;垃圾回收的精确性主要包括a.精确标记或者的对象b.精确地定位对象之间的引用关系。
二、理解面向对象
1.基本特征:
a.封装:将对象实现细节隐藏起来,然后通过一些共用方法来暴露该对象的功能
b.继承:
c.多态:子类对象可以直接赋给父类变量,运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。
三、数据类型和运算符
1.注释,单行注释// ,多行注释 /* .... */,文档注释/** ... */
2.注释中常用标记@author指定java程序作者,@version指定源文件的版本,@deprecated不推荐使用的方法,@param方法的参数说明信息,@return方法的返回值说明信息,@see参见,关于指定交叉参考的内容,@exception抛出异常类型,@throws抛出的异常,和exception同意。
类或接口文档中的标记@see、@deprecated、@author、@version;方法或构造器文档中的标记@see、@deprecated、@param、@return、@throws和@exception等;出现在Field文档注释中的有@see、@deprecated等
package yeeku; /** * Description: * <br/>网站: <a href="http://www.crazyit.org">疯狂Java联盟</a> * <br/>Copyright (C), 2001-2012, Yeeku.H.Lee * <br/>This program is protected by copyright laws. * <br/>Program Name: * <br/>Date: * @author Yeeku.H.Lee [email protected] * @version 1.0 */ public class JavadocTagTest { /** * 一个得到打招呼字符串的方法。 * @param name 该参数指定向谁打招呼。 * @return 返回打招呼的字符串。 */ public String hello(String name) { return name + ",你好!"; } }
4.Java语言指出的类型,基本类型(primitive type):int等,引用类型(reference type):类、接口、数组、null
5.java7中新增了二进制类型。以0b或者0B开头
6.特殊的三个浮点数:正无穷大Double或Float的POSITIVE_INFINITY表示,负无穷大NEGATIVE_INFINITY表示,非数NaN
7.java7中可以在数值中使用下划线。int binVal = 0B1000_0000_0000_0011;double pi = 3.14_15_92;
四、数组
1.数组是一种数据类型,它本身市一中引用类型。 int[]就是一种类型。
2.java数组定义格式 type[] arrayName 和 type arrayName[]; 推荐第一种。这样只定义了一个引用变量,没有分配内存空间。
3.初始化:
a.静态初始化arrayName= new type[] {element1,element2...},type与element类型相同。也可以arrayName = {element1, element2...}
b.动态初始化arrayName= new type[length]
4.foreach用于遍历数组和集合,for (type variableName : array | collection) { .................},只提供访问,不提供修改,即修改无效。
5.基本数据类型数组直接在数组引用的内存存储值,而引用类型数组数组元素是引用,需要再次申请堆内存。
6.二维数组的初始化
public class TwoDimensionTest { public static void main(String[] args) { //定义一个二维数组 int[][] a; //把a当成一维数组进行初始化,初始化a是一个长度为4的数组 //a数组的数组元素又是引用类型 a = new int[4][]; //把a数组当成一维数组,遍历a数组的每个数组元素 for (int i = 0 , len = a.length; i < len ; i++ ) { System.out.println(a[i]); } //初始化a数组的第一个元素 a[0] = new int[2]; //访问a数组的第一个元素所指数组的第二个元素 a[0][1] = 6; //a数组的第一个元素是一个一维数组,遍历这个一维数组 for (int i = 0 , len = a[0].length ; i < len ; i ++ ) { System.out.println(a[0][i]); } //同时初始化二维数组的2个维数 int[][] b = new int[3][4]; //使用静态初始化的语法来初始化一个二维数组 String[][] str1 = new String[][]{new String[3] , new String[]{"hello"}}; //使用简化的静态初始化语法来初始化二维数组 String[][] str2 = {new String[3] , new String[]{"hello"}}; System.out.println(str1[1][0]); System.out.println(str2[1][0]); } }
7.数组操作的工具类Arrays
五、面向对象(上)
1.一个类中常见的三种成员:构造器、Field和方法
2.static修饰的成员不能访问没有static修饰的成员
3.方法传递参数凡是只有一种:值传递。
4.形参个数可变的方法,在最后一个形参的类型后增加三点...,表示该形参可以接受多个参数值,多个参数的值被当成数组传入。
public class Varargs { //定义了形参个数可变的方法 public static void test(int a , String... books) { //books被当成数组处理 for (String tmp : books) { System.out.println(tmp); } //输出整数变量a的值 System.out.println(a); } public static void main(String[] args) { //调用test方法 test(5 , "疯狂Java讲义" , "轻量级Java EE企业应用实战"); } }5.重载:同一个类中包含了两个或两个以上方法的方法名相同,但是形参列表不同。方法返回值类型、修饰符等,与方法重载没有任何关系。可变参数方法重载时,优先选择不可变参数的方法。
6.虽然类实例可以访问static Field,但是建议用类.StaticField来访问。
7.访问控制权限。
8.希望子类重写的方法,可以用protected修饰符
9.父包中要使用子包中的类需要写成完整包路径加类名,使用import后可以直接使用类名。
10.Java默认为所有源文件导入java.lang包下的所有类。
11.import static 可以导入类的static Field
import static java.lang.System.*; import static java.lang.Math.*; public class StaticImportTest { public static void main(String[] args) { //out是java.lang.System类的静态Field,代表标准输出 //PI是java.lang.Math类的静态Field,表示π常量 out.println(PI); //直接调用Math类的sqrt静态方法 out.println(sqrt(256)); } }12. 一旦提供了构造器,则不再使用默认的构造器,通常在提供构造器后要提供一个无参构造器
13.几个构造器中与重叠代码时,可以用一个构造器通过this(x,y)调用另一个构造器
public class Apple { public String name; public String color; public double weight; public Apple() { } //两个参数的构造器 public Apple(String name , String color) { this.name = name; this.color = color; } //三个参数的构造器 public Apple(String name , String color , double weight) { //通过this调用另一个重载的构造器的初始化代码 this(name , color); //下面this引用该构造器正在初始化的Java对象 this.weight = weight; } }14.子类包含与父类同名方法的现象被称为方法重写,重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或者相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;"一大“,子类方法的访问权限应比父类方法更大或者相等
15.父类中的private方法对子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法,即使子类中有相同的private方法,也不是重写
16.子类Field定义了与父类Field同名成员变量,那么子类成员变量会覆盖父类成员变量,但是子类对象初始化时也会为父类成员变量分配存储空间,必须通过super访问
17.子类构造器总会调用一次父类构造器,如果没有显示调用父类构造器,则调用默认的父类构造器,super显示调用父类构造器,必须放在子类构造器的第一行
18.创建任何java对象总是先调用object类的构造器,自顶向下调用构造器
19.Java引用变量有编译时类型和运行时类型两类,编译时类型由定义该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。编译时类型和运行时类型不一致,就可能出现所谓的多态性
20.对象的Field并不具有多态性,总是访问它编译时类型所定义的Field,而不是它运行时类型所定义的Field
21.将父类引用强制转换成子类引用时,需要先用instanceof进行预判
22.设计父类的原则:a.尽量隐藏父类的内部数据,多用private,不让子类直接访问父类;b.不要让子类可以随意访问、修改父类的方法,辅助方法用private,不希望改写用final,希望重写,不希望其他类访问,可以用protected。c.尽量不要在构造器中调用将要被子类重写的方法。
class Base { public Base() { test(); } public void test() //①号test方法 { System.out.println("将被子类重写的方法"); } } public class Sub extends Base { private String name; public void test() //②号test方法 { System.out.println("子类重写父类的方法," + "其name字符串长度" + name.length()); } public static void main(String[] args) { //下面代码会引发空指针异常 Sub s = new Sub(); } }当创建Sub对象时,先执行父类构造器,父类构造器调用了被子类重写方法,则变成调用被子类重写后的方法。即执行2号test方法,此时name Field是null
23.使用final或者将构造器设置为private,则该类无法成为父类,对于后者可以提供静态方法以创建该类的实例
24.当子类需要增加额外属性或者行为方式时,就使用继承
25.初始化块是Java类的第四种成员(Field、方法和构造器),先定义的初始化块先执行,后定义的后执行,用{}表示,初始化块在构造器之前执行
26.创建Java对象时,先为该对象的所有实例Field分配内存(前提是该类已经被加载过)接着成语开始对这些实例变量执行初始化,先执行初始化块或者声明Field时指定的初始值,在执行构造器里指定的初始值。
public class InstanceInitTest { //为a分配内存,再执行初始化块将a Field赋值为6 { a = 6; } //再执行将a Field赋值为9 int a = 9; public static void main(String[] args) { //下面代码将输出9。 System.out.println(new InstanceInitTest().a); } }
27.将多个构造器中相同的代码提取到初始化块中定义,能更好地提高初始化代码的复用,提高整个应用的可维护性
28.在存在继承关系中,先执行父类的初始化块,父类的构造器,再执行子类的初始化块和子类构造器
29.静态初始化代码块使用static,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行,静态初始化块总是比普通初始化块先执行。通常用于对类Field执行初始化处理,而不能对实例Field进行初始化处理。不能访问非静态的Field。静态初始化块也要上溯父类。
class Root { static{ System.out.println("Root的静态初始化块"); } { System.out.println("Root的普通初始化块"); } public Root() { System.out.println("Root的无参数的构造器"); } } class Mid extends Root { static{ System.out.println("Mid的静态初始化块"); } { System.out.println("Mid的普通初始化块"); } public Mid() { System.out.println("Mid的无参数的构造器"); } public Mid(String msg) { //通过this调用同一类中重载的构造器 this(); System.out.println("Mid的带参数构造器,其参数值:" + msg); } } class Leaf extends Mid { static{ System.out.println("Leaf的静态初始化块"); } { System.out.println("Leaf的普通初始化块"); } public Leaf() { //通过super调用父类中有一个字符串参数的构造器 super("疯狂Java讲义"); System.out.println("执行Leaf的构造器"); } } public class Test { public static void main(String[] args) { new Leaf(); new Leaf(); } }30.当JVM第一次主动使用某个类时,系统为在类准备阶段为该类的所有静态Field分配内存;在初始化阶段则负责初始化这些静态Field,初始化静态Field就是执行类初始化代码或者声明类Field时指定的初始值,他们的执行顺去与源代码中的排列顺序相同。
public class StaticInitTest { //先执行静态初始化块将a静态Field赋值为6 static { a = 6; } //再执行将a静态Field赋值为9 static int a = 9; public static void main(String[] args) { //下面代码将输出9。 System.out.println(StaticInitTest.a); } }
即先分配空间,再按排列顺序执行赋值。
六、面向对象(下)
1.==和equals,==用来判断两个对象是否相等(基本类型时,只要值相等,引用类型时,必须指向同一个对象),equals用于判断引用对象值是否相等,通常需要重写,默认的是比较对象的地址,String已经重写了
2.final修饰的类、变量和实例变量不可改变。final修饰的类Field必须在静态初始化块中或声明该Field时指定初始值;实例Field必须在非静态初始化块、声明该Field或构造器中指定初始值。final变量必须初始化后才可以访问。final局部变量,只能初始化一次
3.当final修饰基本类型变量,不能对基本类型变量重新赋值,但对于引用类型变量,只保证这个引用类型变量所引用的地址不会改变,即一直引用同意对象,但这个对象完全可以发生改变
4.当类Field、实例Field或者局部变量使用了final修饰符,并且在定义该变量时指定了初始值,该初始值在编译时就被确定下来,那么就相当于是一个直接量(宏替换)
public class FinalLocalTest { public static void main(String[] args) { //定义一个普通局部变量 final int a = 5; System.out.println(a); } }变量a其实并不存在,执行System.out.println(a)时转换为System.out.println(5)
5.final修饰的方法不可被重写(不希望子类重写父类的某个方法),final修饰的类不能有子类
6.创建不可变类的规则(类似与String,Double):使用private和final修饰符来修饰该类的Field;提供带参数构造器,用于根据传入参数来初始化类里的Field;仅为该类的Field提供Getter方法,不提供Setter方法;如果有必要,重写Object类的hashCode和equals方法,还要保证用equals判断相等的对象的hashCode也相等。
public class Address { private final String detail; private final String postCode; //在构造器里初始化两个实例Field public Address() { this.detail = ""; this.postCode = ""; } public Address(String detail , String postCode) { this.detail = detail; this.postCode = postCode; } //仅为两个实例Field提供getter方法 public String getDetail() { return this.detail; } public String getPostCode() { return this.postCode; } //重写equals方法,判断两个对象是否相等。 public boolean equals(Object obj) { if (this == obj) { return true; } if(obj != null && obj.getClass() == Address.class) { Address ad = (Address)obj; // 当detail和postCode相等时,可认为两个Address对象相等。 if (this.getDetail().equals(ad.getDetail()) && this.getPostCode().equals(ad.getPostCode())) { return true; } } return false; } public int hashCode() { return detail.hashCode() + postCode.hashCode() * 31; } }
7.通过引用新的实例对象,就可以避免不可变类的引用类型的Field域被修改
class Name { private String firstName; private String lastName; public Name(){} public Name(String firstName , String lastName) { this.firstName = firstName; this.lastName = lastName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getFirstName() { return this.firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getLastName() { return this.lastName; } } public class Person { private final Name name; public Person(Name name) { this.name = name; } public Name getName() { return name; } public static void main(String[] args) { Name n = new Name("悟空", "孙"); Person p = new Person(n); // Person对象的name的firstName值为"悟空" System.out.println(p.getName().getFirstName()); // 改变Person对象name的firstName值 n.setFirstName("八戒"); // Person对象的name的firstName值被改为"八戒" System.out.println(p.getName().getFirstName()); } }修改代码为:
public class Person { private final Name name; public Person(Name name) {
this.name =new Name(name.getFirstName()name.getLastName());
} public Name getName() { return newName(name.getFirstName()name.getLastName()); } public static void main(String[] args) { Name n = new Name("悟空", "孙"); Person p = new Person(n); // Person对象的name的firstName值为"悟空" System.out.println(p.getName().getFirstName()); // 改变Person对象name的firstName值 n.setFirstName("八戒"); // Person对象的name的firstName值被改为"八戒" System.out.println(p.getName().getFirstName()); } }8.含有抽象方法的类只能被定义成抽象类(直接定义一个抽象方法;继承了一个抽象父类没有完全实现父类包含的抽象方法,以及实现了一个接口,但没有完全实现接口包含的抽象方法),final和abstract不能同时使用,static不能和abstract同时使用
9.接口中不能包含普通方法,接口中所有方法都是抽象方法。一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。接口里可以包含Field(只能是常量)、方法(只能是抽象实例方法)、内部类(包括内部接口、枚举)定义。接口中的成员(包括常量、方法、内部类和枚举)都是public访问权限,可以省略public,如果指定访问修饰则只能用public。接口中的Field会自动添加static 和 final修饰符,即自动添加public static final,而且接口中没有构造器和初始化块,必须在定义时指定默认值。接口里的方法总是public abstract来修饰。接口里定义的内部类、接口、枚举类默认都采用public static 两个修饰符
10.一个类可以继承一个父类,实现多个接口
11.内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类成员之间可以相互访问,但外部类不能访问内部类的实现细节
12.内部类的文件形式,B是A的内部类,则会生成A.class和A$B.class
13.外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this,外部类类名.this作为限定来区分。同过OutterClass.this.propName的形式访问外部类的实例Field,通过this.propName的形式访问非静态内部类的实例Field
public class DiscernVariable { private String prop = "外部类的实例变量"; private class InClass { private String prop = "内部类的实例变量"; public void info() { String prop = "局部变量"; //通过 外部类类名.this.varName 访问外部类实例Field System.out.println("外部类的Field值:" + DiscernVariable.this.prop); //通过 this.varName 访问内部类实例的Field System.out.println("内部类的Field值:" + this.prop); //直接访问局部变量 System.out.println("局部变量的值:" + prop); } } public void test() { InClass in = new InClass(); in.info(); } public static void main(String[] args) { new DiscernVariable().test(); } }14.内部类也满足静态关系,即静态成员不能访问非静态成员,不允许在非静态内部类里定义静态成员
15.static修饰的内部类,属于外部类本身,称为静态内部类。外部类不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。
public class AccessStaticInnerClass { static class StaticInnerClass { private static int prop1 = 5; private int prop2 = 9; } public void accessInnerProp() { //System.out.println(prop1); //上面代码出现错误,应改为如下形式: //通过类名访问静态内部类的类成员 System.out.println(StaticInnerClass.prop1); //System.out.println(prop2); //上面代码出现错误,应改为如下形式: //通过实例访问静态内部类的实例成员 System.out.println(new StaticInnerClass().prop2); } }
16.接口中定义的内部类,默认使用public static 修饰
17.在外部类以外使用非静态内部类:如果在外部类以外的地方访问内部类(包括静态和非静态),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用,对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用。
OuterClass.InnerClass varName //在外部类意外定义内部类变量在外部类以外的地方创建非静态内部类实例
OuterInstance.new InnerConstructor();
class Out { //定义一个内部类,不使用访问控制符, //即只有同一个包中其他类可访问该内部类 class In { public In(String msg) { System.out.println(msg); } } } public class CreateInnerInstance { public static void main(String[] args) { Out.In in = new Out().new In("测试信息"); /* 上面代码可改为如下三行代码: 使用OutterClass.InnerClass的形式定义内部类变量 Out.In in; 创建外部类实例,非静态内部类实例将寄存在该实例中 Out out = new Out(); 通过外部类实例和new来调用内部类构造器创建非静态内部类实例 in = out.new In("测试信息"); */ } }非静态内部类构造器必须使用外部类对象来调用。
18.在外部类以外使用静态内部类
class StaticOut { //定义一个静态内部类,不使用访问控制符, //即同一个包中其他类可访问该内部类 static class StaticIn { public StaticIn() { System.out.println("静态内部类的构造器"); } } } public class CreateStaticInnerInstance { public static void main(String[] args) { StaticOut.StaticIn in = new StaticOut.StaticIn(); /* 上面代码可改为如下两行代码: 使用OutterClass.InnerClass的形式定义内部类变量 StaticOut.StaticIn in; 通过new来调用内部类构造器创建静态内部类实例 in = new StaticOut.StaticIn(); */ } }静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器
使用静态内部类只要把外部类当成静态内部类的包空间即可。
19.匿名内部类适合创建那种只使用一次的类,匿名内部类会立即创建一个该类的实例,匿名内部类必须继承一个父类,或(最多)实现一个接口。匿名内部类不能是抽象类(需要对象实例),匿名内部类不能定义构造器(无类名),可以定义实例初始化块
new 父类构造器(实参列表) | (实现接口){ 匿名内部类的类体}
interface Product
{ public double getPrice(); public String getName(); } public class AnonymousTest { public void test(Product p) { System.out.println("购买了一个" + p.getName() + ",花掉了" + p.getPrice()); } public static void main(String[] args) { AnonymousTest ta = new AnonymousTest(); //调用test方法时,需要传入一个Product参数, //此处传入其匿名实现类的实例 ta.test(new Product() { public double getPrice() { return 567.8; } public String getName() { return "AGP显卡"; } }); } }20.通过接口创建匿名内部类时,不能显示创建构造器,只有一个隐式的无参构造器,new 接口名后的括号里不能传入参数值;通过继承父类来创建匿名内部类时,匿名内部类自动拥有和父类相似(相同的形参列表)的构造器。
abstract class Device { private String name; public abstract double getPrice(); public Device(){} public Device(String name) { this.name = name; } //此处省略了name的setter和getter方法 public void setName(String name) { this.name = name; } public String getName() { return this.name; } } public class AnonymousInner { public void test(Device d) { System.out.println("购买了一个" + d.getName() + ",花掉了" + d.getPrice()); } public static void main(String[] args) { AnonymousInner ai = new AnonymousInner(); //调用有参数的构造器创建Device匿名实现类的对象 ai.test(new Device("电子示波器") { public double getPrice() { return 67.8; } }); //调用无参数的构造器创建Device匿名实现类的对象 Device d = new Device() { //初始化块 { System.out.println("匿名内部类的初始化块..."); } //实现抽象方法 public double getPrice() { return 56.2; } //重写父类的实例方法 public String getName() { return "键盘"; } }; ai.test(d); } }21. 匿名内部类内部只能访问外部类的final变量
22.回调就是某一个方法一旦获得了内部类对象的引用后,就可以在合适的时候反过来去调用外部类实例的方法。所谓回调,就是允许客户类通过内部类引用来调用其外部类的方法
public class TeachableProgrammer extends Programmer { public TeachableProgrammer(){} public TeachableProgrammer(String name) { super(name); } //教学工作依然由TeachableProgrammer类定义 private void teach() { System.out.println(getName() + "教师在讲台上讲解..."); } private class Closure implements Teachable { /* 非静态内部类回调外部类实现work方法,非静态内部类引用的作用仅仅是 向客户类提供一个回调外部类的途径 */ public void work() { teach(); } } //返回一个非静态内部类引用,允许外部类通过该非静态内部类引用来回调外部类的方法 public Teachable getCallbackReference() { return new Closure(); } }
public class TeachableProgrammerTest { public static void main(String[] args) { TeachableProgrammer tp = new TeachableProgrammer("李刚"); //直接调用TeachableProgrammer类从Programmer类继承到的work方法 tp.work(); //表面上调用的是Closure的work方法, //实际上是回调TeachableProgrammer的teach方法 tp.getCallbackReference().work(); } }23.用enum定义枚举类,默认继承了Enum类,Enum类实现了Serializable和Comparable两个接口。使用enum定义、 非抽象的枚举类默认会使用final修饰,构造器只能使用private访问控制符;枚举类的所有实例必须在枚举类第一行显示列出,系统自动添加public static final修饰;枚举类提供了一个values方法。
public enum SeasonEnum { // 在第一行列出4个枚举实例 SPRING,SUMMER,FALL,WINTER; }
public class EnumTest { public void judge(SeasonEnum s) { //switch语句里的表达式可以是枚举值 switch (s) { case SPRING: System.out.println("春暖花开,正好踏青"); break; case SUMMER: System.out.println("夏日炎炎,适合游泳"); break; case FALL: System.out.println("秋高气爽,进补及时"); break; case WINTER: System.out.println("冬日雪飘,围炉赏雪"); break; } } public static void main(String[] args) { //所有枚举类都有一个values方法,返回该枚举类的所有实例 for (SeasonEnum s : SeasonEnum.values()) { System.out.println(s); } //平常使用枚举实例时, //总是通过EnumClass.variable形式来访问 new EnumTest().judge(SeasonEnum.SPRING); } }23.枚举类的实例只能是枚举值,而不是随意通过new来创建枚举对象。每一个实例拥有一套定义的Field
better
public enum Gender { MALE,FEMALE; private String name; public void setName(String name) { switch (this) { case MALE: if (name.equals("男")) { this.name = name; } else { System.out.println("参数错误"); return; } break; case FEMALE: if (name.equals("女")) { this.name = name; } else { System.out.println("参数错误"); return; } break; } } public String getName() { return this.name; } }
public class GenderTest { public static void main(String[] args) { Gender g = Enum.valueOf(Gender.class , "FEMALE"); g.setName("女"); System.out.println(g + "代表:" + g.getName()); //此时设置name值时将会提示参数错误。 g.setName("男"); System.out.println(g + "代表:" + g.getName()); } }best
public enum Gender { //此处的枚举值必须调用对应构造器来创建 MALE("男"),FEMALE("女"); private final String name; //枚举类的构造器只能使用private修饰 private Gender(String name) { this.name = name; } public String getName() { return this.name; } }24. 枚举类实现接口与普通类一样。这样每个实例都有同样的方法。如果要实现实例方法的差异化,需要让每个枚举值分别来实现该方法
public interface GenderDesc { void info(); }
public enum Gender implements GenderDesc { //此处的枚举值必须调用对应构造器来创建 MALE("男") //花括号部分实际上是一个类体部分 { public void info() { System.out.println("这个枚举值代表男性"); } }, FEMALE("女") { public void info() { System.out.println("这个枚举值代表女性"); } }; //其他部分与codes\06\6.8\best\Gender.java中的Gender类完全相同 }匿名内部类的原理,创建的是Gender的子类实例
public enum Operation2 { PLUS { public double eval(double x , double y) { return x + y; } }, MINUS { public double eval(double x , double y) { return x - y; } }, TIMES { public double eval(double x , double y) { return x * y; } }, DIVIDE { public double eval(double x , double y) { return x / y; } }; //为枚举类定义一个抽象方法, //这个抽象方法由不同枚举值提供不同的实现 public abstract double eval(double x, double y); public static void main(String[] args) { System.out.println(Operation2.PLUS.eval(3, 4)); System.out.println(Operation2.MINUS.eval(5, 4)); System.out.println(Operation2.TIMES.eval(5, 4)); System.out.println(Operation2.DIVIDE.eval(5, 4)); } }6.9 垃圾回收机制回收对象前,会先调用它的finalize方法,该方法可能使该对象重新复活。不要主动调用某个对象的finalize方法;finalize方法何时被调用是否被调用具有不确定性,不一定会执行;finalize方法出现异常时,垃圾回收机制不会报告异常;finalize方法可能使该对象或系统中的其他对象重新可达。
七、与运行环境交互
1.Scanner类是一个基于正则表达式的文本扫描器;System类和Runtime类与程序的运行平台进行交互
2.StringBuilder和StringBuffer是可变字符串,不同的是StringBuffer是线程安全的,而StringBuilder没有实现线程安全,性能更高3.Random类专门用于生成一个伪随机数,一个构造器使用默认的种子(当前时间),另一个构造器需要显示传入long型整数种子。ThreadLocalRandom是Random的增强版,在并发环境下,使用ThreadLocalRandom来代替Random可以减少多线程资源竞争,保证更好的线程安全性
4.Random产生的是伪随机数,相同的种子,会产生相同的随机数,推荐使用当前时间作为Random对象的种子
5.BigDecimal用于避免精度丢失
6.Date类在设计上存在缺陷,Calender类能更好的处理日期和时间,其中add会进退位,而roll不会,setLenient(false)关闭容错性,Month设置13时不会进位。lenient模式可接受超出范围的值,而non-lenient模式则不能,否则将抛出异常。set方法会延迟修改,到真正访问时才更新。TimeZone类用于设置时区。
7.Java国际化与格式化
八、Java集合
1.Java集合类主要由两个接口派生:Collection和Map
2.使用Iterator接口遍历集合元素,Iterator对象依附于Collection对象。使用Iterator迭代访问Collection集合元素时,集合里的元素不能被改变
3.foreach,也不能修改迭代变量的值
public class ForeachTest
{
public static void main(String[] args)
{
//创建一个集合
Collection books = new HashSet();
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
for (Object obj : books)
{
//此处的book变量也不是集合元素本身
String book = (String)obj;
System.out.println(book);
if (book.equals("疯狂Android讲义"))
{
//下面代码会引发ConcurrentModificationException异常
books.remove(book); //①
}
}
System.out.println(books);
}
}
4.Set集合中判断两个对象是否相同不是使用==运算,而是根据equals
5.HashSet需要通过代码来保证同步性,集合元素值可以是null,HashSet是通过equals和hashCode中的一个或者全部来判断元素相同,如果需要把某个类的对象保存到HashSet集合中,重写这个类的equals方法和hashCode方法时,应尽量保证两个对象通过equals方法比较是返回true时,它们的hashCode方法返回值也相等
6.hashCode方法基本规则:同一个对象多次调用hashCode方法应该返回相同的值;equals方法比较返回true时,这两个对象的hashCode方法应该返回相等的值;equals方法比较标准的Field,都应该用来计算hashCode值
将对象内每个有意义的Field计算出一个int类型的hashCode;组合之前计算的多个hashCode值计算出一个hashCode值返回(可以通过为各Field乘以任意一个质数后再相加来避免直接想家产生偶然相等。当向hashSet中添加可变对象时可能导致该独享与集合中的其他对象相等,从而导致HashSet无法准确访问该对象
class R { int count; public R(int count) { this.count = count; } public String toString() { return "R[count:" + count + "]"; } public boolean equals(Object obj) { if(this == obj) return true; if (obj != null && obj.getClass() == R.class) { R r = (R)obj; if (r.count == this.count) { return true; } } return false; } public int hashCode() { return this.count; } } public class HashSetTest2 { public static void main(String[] args) { HashSet hs = new HashSet(); hs.add(new R(5)); hs.add(new R(-3)); hs.add(new R(9)); hs.add(new R(-2)); //打印HashSet集合,集合元素没有重复 System.out.println(hs); //取出第一个元素 Iterator it = hs.iterator(); R first = (R)it.next(); //为第一个元素的count实例变量赋值 first.count = -3; //① //再次输出HashSet集合,集合元素有重复元素 System.out.println(hs); //删除count为-3的R对象 hs.remove(new R(-3)); // //可以看到被删除了一个R元素 System.out.println(hs); //输出false System.out.println("hs是否包含count为-3的R对象?" + hs.contains(new R(-3))); //输出false System.out.println("hs是否包含count为5的R对象?" + hs.contains(new R(5))); } }
先计算hashcode找到桶,再按照equals来判断是否相等,hs.remove(new R(-3));计算该对象的hashcode后与第三个元素所在桶一致,equals也相等则包含。hs.contains(new R(5))计算hashcode与第一个元素所在桶一直,equals则不想等,所以不包含
7.LinkedHashSet是HashSet子类,需要使用链表维护元素的次序;TreeSet是SortedSet接口的实现类,可以确保集合元素处于排序状态;通过实现Comparable接口,来实现对该类对象的比较,在TreeSet中插入元素时,会自动调用元素的compareTo方法,如果添加到TreeSet中的类对象没有compareTo方法,会抛出异常。equals方法返回true时,comparaTo应该返回0
8.可以通过Comparator接口实现定制排序
class M { int age; public M(int age) { this.age = age; } public String toString() { return "M[age:" + age + "]"; } } public class TreeSetTest4 { public static void main(String[] args) { TreeSet ts = new TreeSet(new Comparator() { //根据M对象的age属性来决定大小 public int compare(Object o1, Object o2) { M m1 = (M)o1; M m2 = (M)o2; return m1.age > m2.age ? -1 : m1.age < m2.age ? 1 : 0; } }); ts.add(new M(5)); ts.add(new M(-3)); ts.add(new M(9)); System.out.println(ts); } }创建了一个Comparator接口的匿名内部类对象,该对象负责ts集合的排序,可以无需实现Comparable接口,由TreeSet关联的Comparator对象负责集合元素的排序
9.EnumSet是专为枚举类设计的集合类,其中的元素必须是指定枚举类型的枚举值,其中的元素是有序的,以枚举值在Enum类内的定义顺序来决定集合元素的顺序。不允许加入null
10.HashSet、TreeSet和EnumSet都是线程不安全的,必须手动保证该Set集合的线程安全,通常可以通过Collections工具类sysnchronizedSortedSet方法来“包装”该Set集合
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...))
11.List是Collection接口的子接口,List提供了额外的listIterator()方法,返回一个ListIterator对象,在Iterator基础上增加了一些用于List的方法
12.ArrayList和Vector是List类的两个典型实现,initialCapacity控制内部数组的长度,会自增长,如果知道要保存多少元素可以指定initialCapacity大小。ArrayList是线程不安全的,而Vector是线程安全的,Vector性能较低,使用Collections工具类可以将ArrayList编程线程安全的
13.Arrays.ArrayList是固定长度的List集合,只能访问,不可增加、删除该集合里的元素
14.Queue接口有一个PriorityQueue实现类,元素按大小进行排序,可以通过插入元素实现Comparable接口实现自然排序,也可以创建PriorityQueue队列时传入一个Comparator对象实现定制排序;Queue还有一个Deque接口(双端队列,可以当队列和栈使用),有ArrayDeque和LinkedList两个实现类
15.HashMap和Hashtable实现了Map接口,Hashtable线程安全,且不能使用null作为key和value,HashMap可以使用null作为key或value,HashMap和Hashtable通过key的equals返回true判断两个key是否相等
16.LinkedHashMap链表维护Map的迭代顺序,与插入顺序保持一致
17.EnumMap是一个与枚举类一起使用的Map实现,所有key都必须是单个枚举类的枚举值。根据key的自然顺序来维护key-value对的顺序。创建EnumMap时必须指定一个枚举类
enum Season { SPRING,SUMMER,FALL,WINTER } public class EnumMapTest { public static void main(String[] args) { //创建一个EnumMap对象,该EnumMap的所有key //必须是Season枚举类的枚举值 EnumMap enumMap = new EnumMap(Season.class); enumMap.put(Season.SUMMER , "夏日炎炎"); enumMap.put(Season.SPRING , "春暖花开"); System.out.println(enumMap); } }18.HashSet与HashMap的性能选项:容量-capacity,初始化容量-initial capacity,尺寸-size,负载因子-load factor
19.Collections工具类,提供了排序、查找、替换操作和同步控制(通过synchronizedXxx()方法将指定集合包装成线程同步的集合,解决多线程并发访问集合时的线程安全问题)
public class SynchronizedTest { public static void main(String[] args) { //下面程序创建了四个同步的集合对象 Collection c = Collections .synchronizedCollection(new ArrayList()); List list = Collections.synchronizedList(new ArrayList()); Set s = Collections.synchronizedSet(new HashSet()); Map m = Collections.synchronizedMap(new HashMap()); } }返回不可变集合:emptyXxx()、singletonXxx()、unmodifiableXxx()
public class UnmodifiableTest { public static void main(String[] args) { //创建一个空的、不可改变的List对象 List unmodifiableList = Collections.emptyList(); //创建一个只有一个元素,且不可改变的Set对象 Set unmodifiableSet = Collections.singleton("疯狂Java讲义"); //创建一个普通Map对象 Map scores = new HashMap(); scores.put("语文" , 80); scores.put("Java" , 82); //返回普通Map对象对应的不可变版本 Map unmodifiableMap = Collections.unmodifiableMap(scores); //下面任意一行代码都将引发UnsupportedOperationException异常 unmodifiableList.add("测试元素"); //① unmodifiableSet.add("测试元素"); //② unmodifiableMap.put("语文" , 90); //③ } }
1.创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,父类不能再包含类型形参,必须指定
public class A extends Apple<T> {...} \\错误
public class A extends Apple<String> {...} \\正确子类重写父类方法时需要将T换成具体的类型
public class A1 extends Apple<String> { // 正确重写了父类的方法,返回值 // 与父类Apple<String>的返回值完全相同 public String getInfo() { return "子类" + super.getInfo(); } /* // 下面方法是错误的,重写父类方法时返回值类型不一致 public Object getInfo() { return "子类"; } */ }如果使用Apple时没有传入实际的类型参数,将默认使用Object
2.不管为泛型类传入哪一种类型实参,对于Java来说,他们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参
List<String> l1 = new ArrayList<>(); List<Integer> l2 = new ArrayList<>(); System.out.println(l1.getClass() == l2.getClass()); //实际输出为true
由于系统并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类
3.如果Foo是Bar的一个子类型,而G是具有泛型声明的类或接口, G<Foo>并不是G<Bar>的子类,即List<String>不是List<Object>子类,而数组中Foo[]是Bar[]的子类型4.类型通配符?,List<?>位置类型的List,元素类型可以匹配任何类型,元素为Object,这个List集合可以是任何泛型List的父类
5.被限制的通配符,List<? extends Shape>表示元素为Shape的子类,未知类型一定是Shape子类,Shape为通配符的上限
6.定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。
public class Apple<T extends Number> { T col; public static void main(String[] args) { Apple<Integer> ai = new Apple<>(); Apple<Double> ad = new Apple<>(); // 下面代码将引起编译异常,下面代码试图把String类型传给T形参 // 但String不是Number的子类型,所以引发编译错误 Apple<String> as = new Apple<>(); //① } }更极端的情况需要为类型形参设定多个上限( 至多有一个父类上限,可以有多个接口上限)
public class Apple<T extends Number & java.io.Serializabel> {...}7.泛型方法需要在方法前指明类型参数
修饰符 <T,S> 返回值类型 方法名(形参列表) { ... }
public class GenericMethodTest { // 声明一个泛型方法,该泛型方法中带一个T类型形参, static <T> void fromArrayToCollection(T[] a, Collection<T> c) { for (T o : a) { c.add(o); } } public static void main(String[] args) { Object[] oa = new Object[100]; Collection<Object> co = new ArrayList<>(); // 下面代码中T代表Object类型 <Object> fromArrayToCollection(oa, co); String[] sa = new String[100]; Collection<String> cs = new ArrayList<>(); // 下面代码中T代表String类型 fromArrayToCollection(sa, cs); // 下面代码中T代表Object类型 fromArrayToCollection(sa, co); Integer[] ia = new Integer[100]; Float[] fa = new Float[100]; Number[] na = new Number[100]; Collection<Number> cn = new ArrayList<>(); // 下面代码中T代表Number类型 fromArrayToCollection(ia, cn); // 下面代码中T代表Number类型 fromArrayToCollection(fa, cn); // 下面代码中T代表Number类型 fromArrayToCollection(na, cn); // 下面代码中T代表Object类型 fromArrayToCollection(na, co); // 下面代码中T代表String类型,但na是一个Number数组, // 因为Number既不是String类型, // 也不是它的子类,所以出现编译错误 //fromArrayToCollection(na, cs); } }<>中的T代表泛型参数,而数组的T不代表泛型参数,编译器通过泛型参数来确定类型。不要制造迷惑,系统一旦迷惑,就出错了
public class ErrorTest { // 声明一个泛型方法,该泛型方法中带一个T类型形参 static <T> void test(Collection<T> from, Collection<T> to) { for (T ele : from) { to.add(ele); } } public static void main(String[] args) { List<Object> as = new ArrayList<>(); List<String> ao = new ArrayList<>(); // 下面代码将产生编译错误 test(as , ao); } }改为如下形式即可 static <T> void test( Collection<? extends T> from , Collection<T> to)
public class RightTest { // 声明一个泛型方法,该泛型方法中带一个T形参 static <T> void test(Collection<? extends T> from , Collection<T> to) { for (T ele : from) { to.add(ele); } } public static void main(String[] args) { List<Object> ao = new ArrayList<>(); List<String> as = new ArrayList<>(); // 下面代码完全正常 test(as , ao); } }8.大多数时候可以使用泛型方法来代替类型通配符
9.<? super Type>这个通配符表示它必须是Type本身,或是Type的父类,即设定了通配符的下限
public class MyUtils { // 下面dest集合元素类型必须与src集合元素类型相同,或是其父类 public static <T> T copy(Collection<? super T> dest , Collection<T> src) { T last = null; for (T ele : src) { last = ele; dest.add(ele); } return last; } public static void main(String[] args) { List<Number> ln = new ArrayList<>(); List<Integer> li = new ArrayList<>(); li.add(5); // 此处可准确的知道最后一个被复制的元素是Integer类型 // 与src集合元素的类型相同 Integer last = copy(ln , li); // ① System.out.println(ln); } }这样不会丢失返回类型
十、异常处理
1.Checked异常都是可以在编译阶段被处理的异常,必须强制处理,而Runtime异常无需处理
2.常见的异常类之间的继承关系
Error错误一般指与虚拟机相关的问题,这种错误无法回复或不可能捕获,将导致应用程序中断
3.java7支持多异常捕获,多种类型异常之间用 | 间隔,捕获多种类型的异常时,异常变量有隐式的final修饰
public class MultiExceptionTest { public static void main(String[] args) { try { int a = Integer.parseInt(args[0]); int b = Integer.parseInt(args[1]); int c = a / b; System.out.println("您输入的两个数相除的结果是:" + c ); } catch (IndexOutOfBoundsException|NumberFormatException |ArithmeticException ie) { System.out.println("程序发生了数组越界、数字格式异常、算术异常之一"); // 捕捉多异常时,异常变量默认有final修饰, // 所以下面代码有错: ie = new ArithmeticException("test"); //① } catch (Exception e) { System.out.println("未知异常"); // 捕捉一个类型的异常时,异常变量没有final修饰 // 所以下面代码完全正确。 e = new RuntimeException("test"); //② } } }4.除非在try、catch块中调用了退出虚拟机的方法,否则finally块总会被执行,即使包含return和throw语句,也要先执行finally后再来执行return或throw,如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码
5.在java7中增强了try语句,在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源(必须在程序结束时显示关闭的资源如,数据库链接、网络链接等),try语句在该语句结束时自动关闭这些资源。为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口
public class AutoCloseTest { public static void main(String[] args) throws IOException { try ( // 声明、初始化两个可关闭的资源 // try语句会自动关闭这两个资源。 BufferedReader br = new BufferedReader( new FileReader("AutoCloseTest.java")); PrintStream ps = new PrintStream(new FileOutputStream("a.txt"))) { // 使用两个资源 System.out.println(br.readLine()); ps.println("庄生晓梦迷蝴蝶"); } } }隐式的finally块存在,因此这个try语句可以既没有catch块,也没有finally块。在java7中所有的资源类进行了改写,实现了AutoCloseable或Closeable接口
6.子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
7.自定义异常应该继承Exception基类,自定义Runtime异常,应继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参构造器;另一个是带一个字符串参数的构造器
8.Java7增强throw
public class ThrowTest2 { public static void main(String[] args) // Java 6认为①号代码可能抛出Exception, // 所以此处声明抛出Exception // throws Exception // Java 7会检查①号代码可能抛出异常的实际类型, // 因此此处只需声明抛出FileNotFoundException即可。 throws FileNotFoundException { try { new FileOutputStream("a.txt"); } catch (Exception ex) { ex.printStackTrace(); throw ex; //① } } }9.异常链,捕获一个异常然后接着抛出另一个异常,把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层。
1.JDBC访问示意图
2.Java 7改写了Connection、Statement、ResultSet等接口,它们都继承了AutoCloseable接口,因此他们都可以由try语句来关闭
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
十四、Annotation(注释)
1.Annotation是一个接口,可以通过反射来获取指定程序元素的Annotation对象,然后通过Annotation对象来取得注释里的元数据,访问和处理Annotation的工具统称APT
2.4个基本Annotation:@Override指定方法覆载(强制编译器检查子类必须覆盖父类方法,避免参数错误导致编译正确)@Deprecated表示某个程序元素(类、方法等)已过时,当其他程序使用已过时的类、方法时,编译器将给出警告@SuppressWarnings取消显示指定的编译器警告,并且会一直作用于该元素的子元素@SafeVarargs抑制堆污染警告
3.元Annotation用于修饰其他的Annotation定义。
@Retention只能用于修饰一个Annotation定义,指定被修饰的Annotation可以保留多长时间,包含一个RetentionPolicy类型的value成员变量,必须指定value值为RetentionPolicy中3个静态变量中,R.Class:将Annotation记录在class文件中,运行JVM不再保留Annotation(默认);R.RUNTIME:Annotation记录在class文件中,运行JVM保留Annotation,可通过反射获取该Annotation信息;R.SOURCE:将Annotation保存在源代码中,编译器直接丢弃这种Annotation
@Retention(value=RetentionPolicy.RUNTIME) public @interface Testable{}也可以写为(只有一个成员变量时)
@Retention(RetentionPolicy.RUNTIME) public @interface Testable{}@Target只能修饰一个Annotation定义,指定被修饰的Annotation能用于修饰哪些程序单元,包含一个value成员变量,取值ElementType.XXXX
@Target(ElementType.METHOD) public @interface Testable{}@Documented指定被修饰的Annotation将被javadoc工具提取成文档
4.自定义新的Annotation类型使用@interface关键字
public @interface Test { String name() default "jack"; int age();} @Test(name="xxx", age=6) //使用 public void info() { ... }..................................................
十五、输入、输出
1.File类与平台无关的用来操作文件和目录,不能访问文件本身的内容。提供了访问文件名、文件检测、获取常规文件信息、文件操作、目录操作
2.FilenameFilter接口包含一个accept方法,该方法将依次对指定File的所有子目录或者文件进行迭代
public class FilenameFilterTest { public static void main(String[] args) { File file = new File("."); String[] nameList = file.list(new MyFilenameFilter()); for(String name : nameList) { System.out.println(name); } } } // 实现自己的FilenameFilter实现类 class MyFilenameFilter implements FilenameFilter { public boolean accept(File dir, String name) { // 如果文件名以.java结尾,或者文件对应一个路径,返回true return name.endsWith(".java") || new File(name).isDirectory(); } }3.Java的流主要包括字节流(InputStream、OutputStream)和字符流(Reader、Writer),他们都是抽象的基类。节点流是指与特定的IO设备发生读写数据的流,也称为低级流;处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现读写功能,处理流也称为高级流。
只要使用相同的处理流,程序就可以采用完全相同的输入输出代码访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化。处理流主要以增加缓冲的方式来提高输入输出的效率并且可能提供了一系列便捷的方法来一次输入输出大批量的内容,处理流可以嫁接在任何已存在的流的基础上
4.Java 7中的IO资源类都实现了AutoCloseable接口,可以通过try语句关闭IO流。使用Java的IO流执行输出时,要关闭输出流,关闭输出流除了保证流的物理资源被回收外,可能还可以将输出流缓冲区中的数据flush到物理节点里,因为在执行close方法之前,自动执行输出流的flush方法。关闭输入输出资源时,只需关闭最上层的流即可,系统会自动关闭被该处理流包装的节点流
5.Java输入输出流体系中常用的流分类
6.InputStreamReader和OutputStreamWriter将字节流转换成字符流
7.System.in代表标准输入,是InputStream类的实例,使用InputStreamReader将其转换成字符输入流,再次包装成BufferedReader
public class KeyinTest { public static void main(String[] args) { try( // 将Sytem.in对象转换成Reader对象 InputStreamReader reader = new InputStreamReader(System.in); //将普通Reader包装成BufferedReader BufferedReader br = new BufferedReader(reader)) { String buffer = null; //采用循环方式来一行一行的读取 while ((buffer = br.readLine()) != null) { //如果读取的字符串为"exit",程序退出 if (buffer.equals("exit")) { System.exit(1); } //打印读取的内容 System.out.println("输入内容为:" + buffer); } } catch (IOException ioe) { ioe.printStackTrace(); } } }8.推回输入流PushbackInputStream和PushbackReader,提供了unread方法,将字节、字符推回到缓冲区,从而允许重复读取。这两个推回输入流都带有一个推回缓冲区,读取也是先从推回缓冲区读取,完全读取了推回缓冲区的内容后,还没装满read所需的数组时才会从原输入流中读取。
public class PushbackTest { public static void main(String[] args) { try( // 创建一个PushbackReader对象,指定推回缓冲区的长度为64 PushbackReader pr = new PushbackReader(new FileReader( "PushbackTest.java") , 64)) { char[] buf = new char[32]; // 用以保存上次读取的字符串内容 String lastContent = ""; int hasRead = 0; // 循环读取文件内容 while ((hasRead = pr.read(buf)) > 0) { // 将读取的内容转换成字符串 String content = new String(buf , 0 , hasRead); int targetIndex = 0; // 将上次读取的字符串和本次读取的字符串拼起来, // 查看是否包含目标字符串, 如果包含目标字符串 if ((targetIndex = (lastContent + content) .indexOf("new PushbackReader")) > 0) { // 将本次内容和上次内容一起推回缓冲区 pr.unread((lastContent + content).toCharArray()); // 指定读取前面len个字符 int len = targetIndex > 32 ? 32 : targetIndex; // 再次读取指定长度的内容(就是目标字符串之前的内容) pr.read(buf , 0 , len); // 打印读取的内容 System.out.print(new String(buf , 0 ,len)); System.exit(0); } else { // 打印上次读取的内容 System.out.print(lastContent); // 将本次内容设为上次读取的内容 lastContent = content; } } } catch (IOException ioe) { ioe.printStackTrace(); } } }9.重定向标准输入输出的方法:
public void setErr(PringStream err): public void setIn(InputStream in): public void setOut(PrintStream out):
public class RedirectOut { public static void main(String[] args) { try( // 一次性创建PrintStream输出流 PrintStream ps = new PrintStream(new FileOutputStream("out.txt"))) { // 将标准输出重定向到ps输出流 System.setOut(ps); // 向标准输出输出一个字符串 System.out.println("普通字符串"); // 向标准输出输出一个对象 System.out.println(new RedirectOut()); } catch (IOException ex) { ex.printStackTrace(); } } }
public class RedirectIn { public static void main(String[] args) { try( FileInputStream fis = new FileInputStream("RedirectIn.java")) { // 将标准输入重定向到fis输入流 System.setIn(fis); // 使用System.in创建Scanner对象,用于获取标准输入 Scanner sc = new Scanner(System.in); // 增加下面一行将只把回车作为分隔符 sc.useDelimiter("\n"); // 判断是否还有下一个输入项 while(sc.hasNext()) { // 输出输入项 System.out.println("键盘输入的内容是:" + sc.next()); } } catch (IOException ex) { ex.printStackTrace(); } } }10.Prosess类提供了InputStream getErrorStream():获取子进程的错误流;InputStream getInputStream():获取子进程的输入流;OutputStream getOutputStream():获取子进程的输出流,要站在本程序的角度看子程序来决定是输入还是输出。
public class ReadFromProcess { public static void main(String[] args) throws IOException { // 运行javac命令,返回运行该命令的子进程 Process p = Runtime.getRuntime().exec("javac"); try( // 以p进程的错误流创建BufferedReader对象 // 这个错误流对本程序是输入流,对p进程则是输出流 BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream()))) { String buff = null; // 采取循环方式来读取p进程的错误输出 while((buff = br.readLine()) != null) { System.out.println(buff); } } } }
public class WriteToProcess { public static void main(String[] args) throws IOException { // 运行java ReadStandard命令,返回运行该命令的子进程 Process p = Runtime.getRuntime().exec("java ReadStandard"); try( // 以p进程的输出流创建PrintStream对象 // 这个输出流对本程序是输出流,对p进程则是输入流 PrintStream ps = new PrintStream(p.getOutputStream())) { // 向ReadStandard程序写入内容,这些内容将被ReadStandard读取 ps.println("普通字符串"); ps.println(new WriteToProcess()); } } } // 定义一个ReadStandard类,该类可以接受标准输入, // 并将标准输入写入out.txt文件。 class ReadStandard { public static void main(String[] args) { try( // 使用System.in创建Scanner对象,用于获取标准输入 Scanner sc = new Scanner(System.in); PrintStream ps = new PrintStream( new FileOutputStream("out.txt"))) { // 增加下面一行将只把回车作为分隔符 sc.useDelimiter("\n"); // 判断是否还有下一个输入项 while(sc.hasNext()) { // 输出输入项 ps.println("键盘输入的内容是:" + sc.next()); } } catch(IOException ioe) { ioe.printStackTrace(); } } }11.RandomAccessFile可以自由访问文件的任意位置,既可以输入也可以输出。文件位置指针定位后进行输出会覆盖原有内容,需要借用缓冲方案(数组、临时文件等)来实现追加功能
public class InsertContent { public static void insert(String fileName , long pos , String insertContent) throws IOException { File tmp = File.createTempFile("tmp" , null); tmp.deleteOnExit(); try( RandomAccessFile raf = new RandomAccessFile(fileName , "rw"); // 创建一个临时文件来保存插入点后的数据 FileOutputStream tmpOut = new FileOutputStream(tmp); FileInputStream tmpIn = new FileInputStream(tmp)) { raf.seek(pos); // ------下面代码将插入点后的内容读入临时文件中保存------ byte[] bbuf = new byte[64]; // 用于保存实际读取的字节数 int hasRead = 0; // 使用循环方式读取插入点后的数据 while ((hasRead = raf.read(bbuf)) > 0 ) { // 将读取的数据写入临时文件 tmpOut.write(bbuf , 0 , hasRead); } // ----------下面代码插入内容---------- // 把文件记录指针重新定位到pos位置 raf.seek(pos); // 追加需要插入的内容 raf.write(insertContent.getBytes()); // 追加临时文件中的内容 while ((hasRead = tmpIn.read(bbuf)) > 0 ) { raf.write(bbuf , 0 , hasRead); } } } public static void main(String[] args) throws IOException { insert("InsertContent.java" , 45 , "插入的内容\r\n"); } }12.序列化Serialize,反序列化Deserialize。要序列化必须实现Serializable(标记接口,无需实现任何方法)或Externalizable接口。反序列化要提供Java对象所属类的class文件,无需调用构造器
public class Person implements java.io.Serializable { private String name; private int age; // 注意此处没有提供无参数的构造器! public Person(String name , int age) { System.out.println("有参数的构造器"); this.name = name; this.age = age; } // 省略name与age的setter和getter方法 // name的setter和getter方法 public void setName(String name) { this.name = name; } public String getName() { return this.name; } // age的setter和getter方法 public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } }
public class WriteObject { public static void main(String[] args) { try( // 创建一个ObjectOutputStream输出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("object.txt"))) { Person per = new Person("孙悟空", 500); // 将per对象写入输出流 oos.writeObject(per); } catch (IOException ex) { ex.printStackTrace(); } } }
public class ReadObject { public static void main(String[] args) { try( // 创建一个ObjectInputStream输入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("object.txt"))) { // 从输入流中读取一个Java对象,并将其强制类型转换为Person类 Person p = (Person)ois.readObject(); System.out.println("名字为:" + p.getName() + "\n年龄为:" + p.getAge()); } catch (Exception ex) { ex.printStackTrace(); } } }使用序列化机制向文件中写入了多个java对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取; 当一个可序列化类有多个父类时(直接或间接),这些父类要么有无参数的构造器,要么也是可序列化的,如果父类不可序列化,只是带有无参数的构造器,则父类中定义的Field值不会序列化到二进制流中。
13.如果类的Field类型是引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field的类也是不可序列化的。
14.当多个对象引用了同一个对象时,不会多次序列化引用对象,只是在初次序列化时会序列化该对象,之后直接输出一个序列化编号
假设序列化顺序为t1->t2->per
潜在的问题,先序列化,然后修改引用对象,再序列化时,只会输出一个编号
public class SerializeMutable { public static void main(String[] args) { try( // 创建一个ObjectOutputStream输入流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("mutable.txt")); // 创建一个ObjectInputStream输入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("mutable.txt"))) { Person per = new Person("孙悟空", 500); // 系统会per对象转换字节序列并输出 oos.writeObject(per); // 改变per对象的name Field per.setName("猪八戒"); // 系统只是输出序列化编号,所以改变后的name不会被序列化 oos.writeObject(per); Person p1 = (Person)ois.readObject(); //① Person p2 = (Person)ois.readObject(); //② // 下面输出true,即反序列化后p1等于p2 System.out.println(p1 == p2); // 下面依然看到输出"孙悟空",即改变后的Field没有被序列化 System.out.println(p2.getName()); } catch (Exception ex) { ex.printStackTrace(); } } }15.包含一些敏感信息,或者某个Field类型是不可序列化的,不希望对该Field进行序列化。可以在Field前面使用transient关键字修饰,这样反序列化时该Field将返回0或者null等,就无法取得该Field值。
public class TransientTest { public static void main(String[] args) { try( // 创建一个ObjectOutputStream输出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("transient.txt")); // 创建一个ObjectInputStream输入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("transient.txt"))) { Person per = new Person("孙悟空", 500); // 系统会per对象转换字节序列并输出 oos.writeObject(per); Person p = (Person)ois.readObject(); System.out.println(p.getAge()); } catch (Exception ex) { ex.printStackTrace(); } } }16.Java提供了自定义序列化机制,可以控制如何序列化各Field,甚至完全不序列化某些Field。下列特殊方法用以实现自定义序列化
writeObject方法负责写入特定类的实例状态,以便相应的readObject方法可以恢复它,重写该方法可以完全获得对序列化机制的控制,可以自主决定哪些Field需要序列化,需要怎样序列化,默认会调用out.defaultWriteObject来保存Java对象的各Field,从而可以实现序列化Java对象那个状态的目的。readObject与此相反用于反序列化,默认调用in.defaultReadObjectlai恢复Java对象的非静态和非瞬态Field。当序列化流不完整时,readObjectNoData方法可以用来正确地初始化反序列化的对象,例如接收方使用的反序列化类的版本不同于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改时,系统都会调用readObjectNoData方法来初始化反序列化的对象。
public class Person implements java.io.Serializable { private String name; private int age; // 注意此处没有提供无参数的构造器! public Person(String name , int age) { System.out.println("有参数的构造器"); this.name = name; this.age = age; } // 省略name与age的setter和getter方法 // name的setter和getter方法 public void setName(String name) { this.name = name; } public String getName() { return this.name; } // age的setter和getter方法 public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } private void writeObject(java.io.ObjectOutputStream out) throws IOException { // 将name Field的值反转后写入二进制流 out.writeObject(new StringBuffer(name).reverse()); out.writeInt(age); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { // 将读取的字符串反转后赋给name Field this.name = ((StringBuffer)in.readObject()).reverse() .toString(); this.age = in.readInt(); } }
17.Java的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace方法,如果该方法返回另一个Java对象,则系统转为序列化另一个对象
public class Person implements java.io.Serializable { private String name; private int age; // 注意此处没有提供无参数的构造器! public Person(String name , int age) { System.out.println("有参数的构造器"); this.name = name; this.age = age; } // 省略name与age的setter和getter方法 // name的setter和getter方法 public void setName(String name) { this.name = name; } public String getName() { return this.name; } // age的setter和getter方法 public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } // 重写writeReplace方法,程序在序列化该对象之前,先调用该方法 private Object writeReplace()throws ObjectStreamException { ArrayList<Object> list = new ArrayList<>(); list.add(name); list.add(age); return list; } }
public class ReplaceTest { public static void main(String[] args) { try( // 创建一个ObjectOutputStream输出流 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("replace.txt")); // 创建一个ObjectInputStream输入流 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("replace.txt"))) { Person per = new Person("孙悟空", 500); // 系统将per对象转换字节序列并输出 oos.writeObject(per); // 反序列化读取得到的是ArrayList ArrayList list = (ArrayList)ois.readObject(); System.out.println(list); } catch (Exception ex) { ex.printStackTrace(); } } }18.另一种自定义序列化机制,完全由程序员决定存储和恢复对象数据,Java类必须实现Externalizable接口,接口中定义了readExternal和writeExternal方法。Externalizable接口强制自定义序列化
public class Person implements java.io.Externalizable { private String name; private int age; // 注意此处没有提供无参数的构造器! public Person(String name , int age) { System.out.println("有参数的构造器"); this.name = name; this.age = age; } // 省略name与age的setter和getter方法 // name的setter和getter方法 public void setName(String name) { this.name = name; } public String getName() { return this.name; } // age的setter和getter方法 public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } public void writeExternal(java.io.ObjectOutput out) throws IOException { // 将name Field的值反转后写入二进制流 out.writeObject(new StringBuffer(name).reverse()); out.writeInt(age); } public void readExternal(java.io.ObjectInput in) throws IOException, ClassNotFoundException { // 将读取的字符串反转后赋给name Field this.name = ((StringBuffer)in.readObject()).reverse().toString(); this.age = in.readInt(); } }19.对象的类名、Field都会被序列化;方法、static Field、transient Field都不会被序列化,反序列化对象时必须有序列化对象的class文件;通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取
20.通过添加private static final long serialVersionUID标识该Java类的序列化版本,不人为指定JVM根据类信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同。修改方法、静态Field或瞬态Field,反序列化不受任何影响
21.Channel和Buffer是新IO中(NIO)的两个核心对象。Channel是对传统的输入输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输;Channel与传统的InputStream、OutputStream最大区别在于它提供了一个map方法将一块数据映射到内存。Buffer可以理解成一个容器,本质是一个数组,发送到Channel或者从Channel中取出的数据必须先放到Channel中
22.Buffer是一个抽象类,有除boolean外的XxxBuffer类,通过static XxxBuffer allocate(int capacity):创建一个容量为capacity的XxxBuffer对象。ByteBuffer类还有一个子类MappedByteBuffer,用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,通常由Channel的map()方法返回。
23.Buffer中三个重要概念:容量capacity,表示Buffer的最大数据容量,创建后不能更改;界限limit,第一个不应该被读出或者写入的缓冲区位置索引;位置position,用于指明下一个可以被读出或者写入的缓冲区位置索引。flip()方法将limit设置为position所在的位置,并将position设为0为输出做好了准备;clear()将position设置为0,将limit设为capacity为装入数据做好准备。put()和get()方法,用于向Buffer中放入和取出数据
public class BufferTest { public static void main(String[] args) { // 创建Buffer CharBuffer buff = CharBuffer.allocate(8); //① System.out.println("capacity: " + buff.capacity()); System.out.println("limit: " + buff.limit()); System.out.println("position: " + buff.position()); // 放入元素 buff.put('a'); buff.put('b'); buff.put('c'); //② System.out.println("加入三个元素后,position = " + buff.position()); // 调用flip()方法 buff.flip(); //③ System.out.println("执行flip()后,limit = " + buff.limit()); System.out.println("position = " + buff.position()); // 取出第一个元素 System.out.println("第一个元素(position=0):" + buff.get()); // ④ System.out.println("取出一个元素后,position = " + buff.position()); // 调用clear方法 buff.clear(); //⑤ System.out.println("执行clear()后,limit = " + buff.limit()); System.out.println("执行clear()后,position = " + buff.position()); System.out.println("执行clear()后,缓冲区内容并没有被清除:" + "第三个元素为:" + buff.get(2)); // ⑥ System.out.println("执行绝对读取后,position = " + buff.position()); } }24.程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。Channel是通过节点流进行的getChannel方法来返回对应的Channel。Channel最常用的3类方法map()映射到Buffer,read()读取Buffer数据,write()将数据写入Buffer中
public class FileChannelTest { public static void main(String[] args) { File f = new File("FileChannelTest.java"); try( // 创建FileInputStream,以该文件输入流创建FileChannel FileChannel inChannel = new FileInputStream(f).getChannel(); // 以文件输出流创建FileBuffer,用以控制输出 FileChannel outChannel = new FileOutputStream("a.txt") .getChannel()) { // 将FileChannel里的全部数据映射成ByteBuffer MappedByteBuffer buffer = inChannel.map(FileChannel .MapMode.READ_ONLY , 0 , f.length()); // ① // 使用GBK的字符集来创建解码器 Charset charset = Charset.forName("GBK"); // 直接将buffer里的数据全部输出 outChannel.write(buffer); // ② // 再次调用buffer的clear()方法,复原limit、position的位置 buffer.clear(); // 创建解码器(CharsetDecoder)对象 CharsetDecoder decoder = charset.newDecoder(); // 使用解码器将ByteBuffer转换成CharBuffer CharBuffer charBuffer = decoder.decode(buffer); // CharBuffer的toString方法可以获取对应的字符串 System.out.println(charBuffer); } catch (IOException ex) { ex.printStackTrace(); } } }25.Java 7 新增了一个StandardCharsets类,包含了最常用字符集对应的Charset静态对象
public class CharsetTransform { public static void main(String[] args) throws Exception { // 创建简体中文对应的Charset Charset cn = Charset.forName("GBK"); // 获取cn对象对应的编码器和解码器 CharsetEncoder cnEncoder = cn.newEncoder(); CharsetDecoder cnDecoder = cn.newDecoder(); // 创建一个CharBuffer对象 CharBuffer cbuff = CharBuffer.allocate(8); cbuff.put('孙'); cbuff.put('悟'); cbuff.put('空'); cbuff.flip(); // 将CharBuffer中的字符序列转换成字节序列 ByteBuffer bbuff = cnEncoder.encode(cbuff); // 循环访问ByteBuffer中的每个字节 for (int i = 0; i < bbuff.capacity() ; i++) { System.out.print(bbuff.get(i) + " "); } // 将ByteBuffer的数据解码成字符序列 System.out.println("\n" + cnDecoder.decode(bbuff)); } }也可以直接用Charset对象直接调用encode,decode方法进行编码解码(无需生成CharsetDecoder和CharsetEncoder对象)
26.FileLock来支持文件锁定功能,FileChannel提供lock(),tryLock()方法可以获得文件锁对象,lock如果无法得到文件锁将阻塞,而tryLock则返回null。release方法释放文件锁
public class FileLockTest { public static void main(String[] args) throws Exception { try( // 使用FileOutputStream获取FileChannel FileChannel channel = new FileOutputStream("a.txt") .getChannel()) { // 使用非阻塞式方式对指定文件加锁 FileLock lock = channel.tryLock(); // 程序暂停10s Thread.sleep(10000); // 释放锁 lock.release(); } } }27.Java 7 中的NIO有重大改进,提供了全面的文件IO和文件系统访问支持以及基于异步Channel的IO。Path接口代表一个平台无关的平台路径;Files工具类包含了大量静态的工具方法来操作文件;Paths包含了两个返回Path的静态工厂方法
28.Files工具类中的FileVisitor遍历文件和目录,可以通过继承SimpleFileVisitor实现自己的文件访问器。
public class FileVisitorTest { public static void main(String[] args) throws Exception { // 遍历g:\publish\codes\15目录下的所有文件和子目录 Files.walkFileTree(Paths.get("g:", "publish" , "codes" , "15") , new SimpleFileVisitor<Path>() { // 访问文件时候触发该方法 @Override public FileVisitResult visitFile(Path file , BasicFileAttributes attrs) throws IOException { System.out.println("正在访问" + file + "文件"); // 找到了FileInputStreamTest.java文件 if (file.endsWith("FileInputStreamTest.java")) { System.out.println("--已经找到目标文件--"); return FileVisitResult.TERMINATE; } return FileVisitResult.CONTINUE; } // 开始访问目录时触发该方法 @Override public FileVisitResult preVisitDirectory(Path dir , BasicFileAttributes attrs) throws IOException { System.out.println("正在访问:" + dir + " 路径"); return FileVisitResult.CONTINUE; } }); } }遍历g:\publish\codes\15目录下所有文件和子目录,找到文件以FileVisitorTest.java结尾,停止遍历
29.WatchService监控文件的变化
30.访问文件的属性,java 7 的NIO.2在java.nio.file.attribute包下提供了工具类可以简单地读取、修改文件属性
十六、多线程
1.进程是系统进行资源分配和调度的一个独立单位,线程是进程的执行单元,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。一个线程可以创建和撤销另一个线程。Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例
2.继承Thread类创建线程类:定义Thread类的子类,并重写该类的run方法,run方法的方法体就代表了线程需要完成的任务;创建Thread子类的实例;调用线程对象的start方法来启动该线程。主线程由main方法确定,子线程不能共享实例属性。
public class FirstThread extends Thread { private int i ; // 重写run方法,run方法的方法体就是线程执行体 public void run() { for ( ; i < 100 ; i++ ) { // 当线程类继承Thread类时,直接使用this即可获取当前线程 // Thread对象的getName()返回当前该线程的名字 // 因此可以直接调用getName()方法返回当前线程的名 System.out.println(getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { // 调用Thread的currentThread方法获取当前线程 System.out.println(Thread.currentThread().getName() + " " + i); if (i == 20) { // 创建、并启动第一条线程 new FirstThread().start(); // 创建、并启动第二条线程 new FirstThread().start(); } } } }3.实现Runnable接口创建线程类:定义Runnable接口的实现类,并重写该接口的run方法;创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象,创建Thread对象时为该Thread对象指定一个名字;调用线程对象的start方法来启动该线程。子线程可以共享实例属性
public class SecondThread implements Runnable { private int i ; // run方法同样是线程执行体 public void run() { for ( ; i < 100 ; i++ ) { // 当线程类实现Runnable接口时, // 如果想获取当前线程,只能用Thread.currentThread()方法。 System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 20) { SecondThread st = new SecondThread(); // ① // 通过new Thread(target , name)方法创建新线程 new Thread(st , "新线程1").start(); new Thread(st , "新线程2").start(); } } } }4.使用Callable和Future创建线程:Callable接口是Runnable接口的增强版,提供了一个call方法作为线程执行体,call方法可以有返回值,也可以抛出异常。Future接口来代表Callable接口里call方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并且实现了Runnable接口可以作为Thread类的target。
步骤如下:创建Callable接口的实现类,并实现call方法;创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call方法的返回值;使用FutureTask对象作为Thread对象的target创建并启动新线程;调用FutureTask对象的get方法来获得子线程执行结束后的返回值。
Callable接口有泛型限制,Callable接口里的泛型形参类型与call方法返回值类型相同
// 实现Callable接口来实现线程 public class ThirdThread implements Callable<Integer> { // 实现call方法,作为线程执行体 public Integer call() { int i = 0; for ( ; i < 100 ; i++ ) { System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i); } // call()方法可以有返回值 return i; } public static void main(String[] args) { // 创建Callable对象 ThirdThread rt = new ThirdThread(); // 使用FutureTask来包装Callable对象 FutureTask<Integer> task = new FutureTask<Integer>(rt); for (int i = 0 ; i < 100 ; i++) { System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i); if (i == 20) { // 实质还是以Callable对象来创建、并启动线程 new Thread(task , "有返回值的线程").start(); } } try { // 获取线程返回值 System.out.println("子线程的返回值:" + task.get()); } catch (Exception ex) { ex.printStackTrace(); } } }4.创建线程的三种方式对比(将实现Runnable接口与实现Callable接口归为一种方式后有两种)
采用实现Runnable、Callable接口的方式创建多线程:线程类只是实现了Runnable接口或Callable接口,还可以继承其他类;多个线程可以共享同一个target对象,非常适合
5.Thread提供join方法让一个线程等待另一个线程完成。当某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,知道被join方法加入的join线程执行完为止
public class JoinThread extends Thread { // 提供一个有参数的构造器,用于设置该线程的名字 public JoinThread(String name) { super(name); } // 重写run方法,定义线程执行体 public void run() { for (int i = 0; i < 100 ; i++ ) { System.out.println(getName() + " " + i); } } public static void main(String[] args)throws Exception { // 启动子线程 new JoinThread("新线程").start(); for (int i = 0; i < 100 ; i++ ) { if (i == 20) { JoinThread jt = new JoinThread("被Join的线程"); jt.start(); // main线程调用了jt线程的join()方法,main线程 // 必须等jt执行结束才会向下执行 jt.join(); } System.out.println(Thread.currentThread().getName() + " " + i); } } }main和新线程并发执行->新线程和被Join的线程并发执行,main等待
6.如果所有的前台线程都死亡,后台线程会自动死亡。Thread对象的setDaemon(true)可将指定线程设置成后台线程
public class DaemonThread extends Thread { // 定义后台线程的线程执行体与普通线程没有任何区别 public void run() { for (int i = 0; i < 1000 ; i++ ) { System.out.println(getName() + " " + i); } } public static void main(String[] args) { DaemonThread t = new DaemonThread(); // 将此线程设置成后台线程 t.setDaemon(true); // 启动后台线程 t.start(); for (int i = 0 ; i < 10 ; i++ ) { System.out.println(Thread.currentThread().getName() + " " + i); } // -----程序执行到此处,前台线程(main线程)结束------ // 后台线程也应该随之结束 } }7.sleep方法让正在执行的线程暂停一段时间,yield方法不会阻塞该线程,将该线程转入就绪状态,只是让当前线程暂停一下,让系统的线程调度器重新调度一次。只有优先级>=当前线程并且出于就绪状态的线程才会获得执行的机会
public class YieldTest extends Thread { public YieldTest(String name) { super(name); } // 定义run方法作为线程执行体 public void run() { for (int i = 0; i < 50 ; i++ ) { System.out.println(getName() + " " + i); // 当i等于20时,使用yield方法让当前线程让步 if (i == 20) { Thread.yield(); } } } public static void main(String[] args)throws Exception { // 启动两条并发线程 YieldTest yt1 = new YieldTest("高级"); // 将ty1线程设置成最高优先级 // yt1.setPriority(Thread.MAX_PRIORITY); yt1.start(); YieldTest yt2 = new YieldTest("低级"); // 将yt2线程设置成最低优先级 // yt2.setPriority(Thread.MIN_PRIORITY); yt2.start(); } }8.线程的优先级在默认情况下与父进程的优先级相同。Thread提供了setPriority和getPriority方法来设置和返回指定线程的优先级,setPriority参数是一个1-10之间的整数,Thread类提供静态常量MAX_PRIORITY=10,MIN_PRIORITY=1,NORM_PRIORITY=5,1-10与操作系统实现有关,最好是用静态常量
9.synchronized(obj) {...}同步代码快,obj为同步监视器。同步监视器用于阻止两个线程对同一个共享资源进行并发访问,因此通常使用可能被并发访问的共享资源充当同步监视器。取钱程序中的账户可作为同步监视器
public class DrawThread extends Thread { // 模拟用户账户 private Account account; // 当前取钱线程所希望取的钱数 private double drawAmount; public DrawThread(String name , Account account , double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } // 当多条线程修改同一个共享数据时,将涉及数据安全问题。 public void run() { // 使用account作为同步监视器,任何线程进入下面同步代码块之前, // 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它 // 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑 synchronized (account) { // 账户余额大于取钱数目 if (account.getBalance() >= drawAmount) { // 吐出钞票 System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount); try { Thread.sleep(1); } catch (InterruptedException ex) { ex.printStackTrace(); } // 修改余额 account.setBalance(account.getBalance() - drawAmount); System.out.println("\t余额为: " + account.getBalance()); } else { System.out.println(getName() + "取钱失败!余额不足!"); } } //同步代码块结束,该线程释放同步锁 } }
10.使用synchronized修饰某个方法,则该方法称为同步方法,同步方法的同步监视器为this(默认指定)。使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征:该类的对象可以被多个线程安全的访问;每个线程调用该对象的任意方法之后都将得到正确的结果;每个线程调用该对象的任意方法后,该对象状态依然保持合理状态。不可变类总是线程安全的,因为它的对象状态不可改变,但可变对象需要额外的方法来保证其线程安全。在取款程序中,只需要将balance的方法修改成同步方法即可
public class Account { // 封装账户编号、账户余额两个Field private String accountNo; private double balance; public Account(){} // 构造器 public Account(String accountNo , double balance) { this.accountNo = accountNo; this.balance = balance; } // accountNo的setter和getter方法 public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } // 因此账户余额不允许随便修改,所以只为balance提供getter方法, public double getBalance() { return this.balance; } // 提供一个线程安全draw()方法来完成取钱操作 public synchronized void draw(double drawAmount) { // 账户余额大于取钱数目 if (balance >= drawAmount) { // 吐出钞票 System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount); try { Thread.sleep(1); } catch (InterruptedException ex) { ex.printStackTrace(); } // 修改余额 balance -= drawAmount; System.out.println("\t余额为: " + balance); } else { System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!"); } } // 下面两个方法根据accountNo来重写hashCode()和equals()方法 public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if(this == obj) return true; if (obj !=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }任一时刻只能有一个线程获得对Account对象的锁定(同步监视器this),然后执行取钱操作
public class DrawThread extends Thread { // 模拟用户账户 private Account account; // 当前取钱线程所希望取的钱数 private double drawAmount; public DrawThread(String name , Account account , double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } // 当多条线程修改同一个共享数据时,将涉及数据安全问题。 public void run() { // 直接调用account对象的draw方法来执行取钱 // 同步方法的同步监视器是this,this代表调用draw()方法的对象。 // 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。 account.draw(drawAmount); } }11.可变类的线程安全是以降低程序的运行效率为代价的,可采用如下策略减少线程安全所带来的负面影响:不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步;如果可变类有两种运行环境:单线程和多线程,则应该为该可变类提供两种版本,线程安全与线程不安全版本(如StringBuilder保证性能,StringBuffer保证线程安全)
13.如下情况,线程不会释放同步监视器:线程执行同步代码块或同步方法时,程序调用sleep、yield方法来暂停当前线程的执行,当前线程不会释放同步监视器;线程执行同步代码快时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。尽量避免使用suspend和resume方法来控制线程
14.同步锁Lock提供了比syschronized方法和代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。某些锁允许对共享资源并发访问,如ReadWriteLock,Lock、ReadWriteLock是Java5新提供大的两个根接口,并未Lock提供了ReentrantLock(可重入锁)实现类;为ReadWriteLock提供了ReentrantReadWriteLock实现类
public class Account { // 定义锁对象 private final ReentrantLock lock = new ReentrantLock(); // 封装账户编号、账户余额两个Field private String accountNo; private double balance; public Account(){} // 构造器 public Account(String accountNo , double balance) { this.accountNo = accountNo; this.balance = balance; } // accountNo的setter和getter方法 public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } // 因此账户余额不允许随便修改,所以只为balance提供getter方法, public double getBalance() { return this.balance; } // 提供一个线程安全draw()方法来完成取钱操作 public void draw(double drawAmount) { // 加锁 lock.lock(); try { // 账户余额大于取钱数目 if (balance >= drawAmount) { // 吐出钞票 System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount); try { Thread.sleep(1); } catch (InterruptedException ex) { ex.printStackTrace(); } // 修改余额 balance -= drawAmount; System.out.println("\t余额为: " + balance); } else { System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!"); } } finally { // 修改完成,释放锁 lock.unlock(); } } // 下面两个方法根据accountNo来重写hashCode()和equals()方法 public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if(this == obj) return true; if (obj !=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }每个Lock对象对应一个Account对象。ReentrantLock锁具有可重入性,一个线程可以对已被加锁的ReentrantLock锁再次加锁
15.死锁案例
class A { public synchronized void foo( B b ) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo方法" ); //① try { Thread.sleep(200); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last方法"); //③ b.last(); } public synchronized void last() { System.out.println("进入了A类的last方法内部"); } } class B { public synchronized void bar( A a ) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar方法" ); //② try { Thread.sleep(200); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法"); //④ a.last(); } public synchronized void last() { System.out.println("进入了B类的last方法内部"); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主线程"); // 调用a对象的foo方法 a.foo(b); System.out.println("进入了主线程之后"); } public void run() { Thread.currentThread().setName("副线程"); // 调用b对象的bar方法 b.bar(a); System.out.println("进入了副线程之后"); } public static void main(String[] args) { DeadLock dl = new DeadLock(); // 以dl为target启动新线程 new Thread(dl).start(); // 调用init()方法 dl.init(); } }16.传统的线程通信借助Object类提供的wait、notify、notifyAll这3个方法,必须由同步监视器对象来调用。syschronized同步方法,该类的默认实例this就是同步监视器,可以在同步方法中直接调用这3个方法;synchronized同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这3个方法。
wait导致当前线程等待,直到其他线程调用该同步监视器的notify方法或notifyAll方法来唤醒该线程,调用wait方法当前线程会释放对于该同步监视器的锁定;
notify唤醒在此同步监视器上等待的单个线程,任意选择一个唤醒,只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程;
notifyAll唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
public class Account { // 封装账户编号、账户余额两个Field private String accountNo; private double balance; //标识账户中是否已有存款的旗标 private boolean flag = false; public Account(){} // 构造器 public Account(String accountNo , double balance) { this.accountNo = accountNo; this.balance = balance; } // accountNo的setter和getter方法 public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } // 因此账户余额不允许随便修改,所以只为balance提供getter方法, public double getBalance() { return this.balance; } public synchronized void draw(double drawAmount) { try { // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞 if (!flag) { wait(); } else { // 执行取钱 System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount); balance -= drawAmount; System.out.println("账户余额为:" + balance); // 将标识账户是否已有存款的旗标设为false。 flag = false; // 唤醒其他线程 notifyAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } } public synchronized void deposit(double depositAmount) { try { // 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞 if (flag) //① { wait(); } else { // 执行存款 System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount); balance += depositAmount; System.out.println("账户余额为:" + balance); // 将表示账户是否已有存款的旗标设为true flag = true; // 唤醒其他线程 notifyAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } } // 下面两个方法根据accountNo来重写hashCode()和equals()方法 public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if(this == obj) return true; if (obj !=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }
public class DrawTest { public static void main(String[] args) { // 创建一个账户 Account acct = new Account("1234567" , 0); new DrawThread("取钱者" , acct , 800).start(); new DepositThread("存款者甲" , acct , 800).start(); new DepositThread("存款者乙" , acct , 800).start(); new DepositThread("存款者丙" , acct , 800).start(); } }
public class DepositThread extends Thread { // 模拟用户账户 private Account account; // 当前取钱线程所希望存款的钱数 private double depositAmount; public DepositThread(String name , Account account , double depositAmount) { super(name); this.account = account; this.depositAmount = depositAmount; } // 重复100次执行存款操作 public void run() { for (int i = 0 ; i < 100 ; i++ ) { account.deposit(depositAmount); } } }
public class DrawTest { public static void main(String[] args) { // 创建一个账户 Account acct = new Account("1234567" , 0); new DrawThread("取钱者" , acct , 800).start(); new DepositThread("存款者甲" , acct , 800).start(); new DepositThread("存款者乙" , acct , 800).start(); new DepositThread("存款者丙" , acct , 800).start(); } }
17.Condition类让已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。Condition将同步监视器方法(wait、notify、notifyAll方法分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集。在这种情况下,Lock代替了同步方法或者同步代码块,Condition替代了同步监视器的功能。Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition方法即可。Condition类提供了如下3个方法。
await:类似于隐式同步监视器上的wait方法,导致当前线程等待,知道其他线程调用该Condition的signal方法或signalAll方法来唤醒该线程,await方法有更多变体。。。
signal:唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程使用await方法放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
singalAll:唤醒在此Lock对象上等待的所有线程,只有当前线程使用await方法放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
public class Account { // 显式定义Lock对象 private final Lock lock = new ReentrantLock(); // 获得指定Lock对象对应的Condition private final Condition cond = lock.newCondition(); // 封装账户编号、账户余额两个Field private String accountNo; private double balance; //标识账户中是否已有存款的旗标 private boolean flag = false; public Account(){} // 构造器 public Account(String accountNo , double balance) { this.accountNo = accountNo; this.balance = balance; } // accountNo的setter和getter方法 public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } // 因此账户余额不允许随便修改,所以只为balance提供getter方法, public double getBalance() { return this.balance; } public void draw(double drawAmount) { // 加锁 lock.lock(); try { // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞 if (!flag) { cond.wait(); } else { // 执行取钱 System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount); balance -= drawAmount; System.out.println("账户余额为:" + balance); // 将标识账户是否已有存款的旗标设为false。 flag = false; // 唤醒其他线程 cond.signalAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } // 使用finally块来释放锁 finally { lock.unlock(); } } public void deposit(double depositAmount) { lock.lock(); try { // 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞 if (flag) //① { cond.wait(); } else { // 执行存款 System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount); balance += depositAmount; System.out.println("账户余额为:" + balance); // 将表示账户是否已有存款的旗标设为true flag = true; // 唤醒其他线程 cond.signalAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } // 使用finally块来释放锁 finally { lock.unlock(); } } // 下面两个方法根据accountNo来重写hashCode()和equals()方法 public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if(this == obj) return true; if (obj !=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }
显示地使用Lock对象来充当同步监视器,需要使用Condition对象来暂停、唤醒指定线程
18.使用阻塞队列控制线程通信BlockingQueue是Queue的子接口,作为线程同步的工具。当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞
class Producer extends Thread { private BlockingQueue<String> bq; public Producer(BlockingQueue<String> bq) { this.bq = bq; } public void run() { String[] strArr = new String[] { "Java", "Struts", "Spring" }; for (int i = 0 ; i < 999999999 ; i++ ) { System.out.println(getName() + "生产者准备生产集合元素!"); try { Thread.sleep(200); // 尝试放入元素,如果队列已满,线程被阻塞 bq.put(strArr[i % 3]); } catch (Exception ex){ex.printStackTrace();} System.out.println(getName() + "生产完成:" + bq); } } } class Consumer extends Thread { private BlockingQueue<String> bq; public Consumer(BlockingQueue<String> bq) { this.bq = bq; } public void run() { while(true) { System.out.println(getName() + "消费者准备消费集合元素!"); try { Thread.sleep(200); // 尝试取出元素,如果队列已空,线程被阻塞 bq.take(); } catch (Exception ex){ex.printStackTrace();} System.out.println(getName() + "消费完成:" + bq); } } } public class BlockingQueueTest2 { public static void main(String[] args) { // 创建一个容量为3的BlockingQueue BlockingQueue<String> bq = new ArrayBlockingQueue<>(1); // 启动3条生产者线程 new Producer(bq).start(); new Producer(bq).start(); new Producer(bq).start(); // 启动一条消费者线程 new Consumer(bq).start(); } }
19.ThreadGroup表示线程组,对一批线程进行分类管理,用户创建的所有线程都属于指定线程组,未指定线程组则属于默认线程组,默认情况下,子线程和父线程处于统一线程组。一旦指定线程组将无法修改,直到线程死亡。Thread类提供了构造器来设置新创建的线程属于哪个线程组,ThreadGroup类提供了构造器来创建实例
class MyThread extends Thread { // 提供指定线程名的构造器 public MyThread(String name) { super(name); } // 提供指定线程名、线程组的构造器 public MyThread(ThreadGroup group , String name) { super(group, name); } public void run() { for (int i = 0; i < 20 ; i++ ) { System.out.println(getName() + " 线程的i变量" + i); } } } public class ThreadGroupTest { public static void main(String[] args) { // 获取主线程所在的线程组,这是所有线程默认的线程组 ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); System.out.println("主线程组的名字:" + mainGroup.getName()); System.out.println("主线程组是否是后台线程组:" + mainGroup.isDaemon()); new MyThread("主线程组的线程").start(); ThreadGroup tg = new ThreadGroup("新线程组"); tg.setDaemon(true); System.out.println("tg线程组是否是后台线程组:" + tg.isDaemon()); MyThread tt = new MyThread(tg , "tg组的线程甲"); tt.start(); new MyThread(tg , "tg组的线程乙").start(); } }20.Thread.UncaughtExceptionHandler是Thread类的一个静态内部接口,该接口只有一个方法uncaughtException。Thread类提供了setDefaultUncaughtExceptionHandler和setUncaughtExceptionHandler。ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器。当一个线程抛出未处理异常时,JVM会首先查找该异常对应的异常处理器,(setUn。。。方法设置的异常处理器),如果找到该异常处理器,则将调用该异常处理器处理该异常;否则,JVM将会调用该线程所属的线程组对象的uncaughtException方法来处理该异常
class MyExHandler implements Thread.UncaughtExceptionHandler { //实现uncaughtException方法,该方法将处理线程的未处理异常 public void uncaughtException(Thread t, Throwable e) { System.out.println(t + " 线程出现了异常:" + e); } } public class ExHandler { public static void main(String[] args) { // 设置主线程的异常处理器 Thread.currentThread().setUncaughtExceptionHandler (new MyExHandler()); int a = 5 / 0; //① System.out.println("程序正常结束!"); } }当使用catch捕获异常时,异常不会向上传播给上一级调用者;但是使用异常处理器对异常进行处理后,异常依然会传播给上一级调用者
21.线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或者Callable对象传给线程池,线程池就会启动一个线程来执行他们的run或call方法,当run或call方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run或call方法。线程池可以控制最大并发线程数。
通过Executors工厂类来产生线程池,newCachedThreadPool,newFixedThreadPool(int nThreads),new SingleThreadExecutor()返回ExecutorService对象,代表一个线程池;newScheduledThreadPool(int corePoolSize),new SingleThreadScheduledExecutor()返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,指定延迟后执行线程任务。用完线程池后,调用线程池的shutdown方法启动线程池的关闭序列,此后不能再接收新任务,但会将之前已经提交的任务执行完成;调用shutdownNow方法关闭线程池将试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务。
22.使用线程池来执行线程任务的步骤如下:调用Executors类的静态工厂方法创建一个ExecutorService对象;创建Runnable或Callable实现,作为线程执行任务;调用ExecutorService对象的submit方法来提交Runnable或Callable实例;不想提交任何任务时,调用Executorservice对象的shutdown方法来关闭线程池。
class MyThread implements Runnable { public void run() { for (int i = 0; i < 100 ; i++ ) { System.out.println(Thread.currentThread().getName() + "的i值为:" + i); } } } public class ThreadPoolTest { public static void main(String[] args) throws Exception { // 创建一个具有固定线程数(6)的线程池 ExecutorService pool = Executors.newFixedThreadPool(6); // 向线程池中提交两个线程 pool.submit(new MyThread()); pool.submit(new MyThread()); // 关闭线程池 pool.shutdown(); } }23.Java7提供了ForkJoinPool来支持将一个任务拆分成多个小任务并行计算,再把多个小任务的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类。提供了两个常用的构造器,在创建了ForkJoinPool实例后,就可以调用submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定任务了。ForkJoinTask是一个抽象类,它还有两个子类:RecursiveAction和RecursiveTask,后者有返回值
// 继承RecursiveAction来实现"可分解"的任务 class PrintTask extends RecursiveAction { // 每个“小任务”只最多只打印50个数 private static final int THRESHOLD = 50; private int start; private int end; // 打印从start到end的任务 public PrintTask(int start, int end) { this.start = start; this.end = end; } @Override protected void compute() { // 当end与start之间的差小于THRESHOLD时,开始打印 if(end - start < THRESHOLD) { for (int i = start ; i < end ; i++ ) { System.out.println(Thread.currentThread().getName() + "的i值:" + i); } } else { // 如果当end与start之间的差大于THRESHOLD时,即要打印的数超过50个 // 将大任务分解成两个小任务。 int middle = (start + end) /2; PrintTask left = new PrintTask(start, middle); PrintTask right = new PrintTask(middle, end); // 并行执行两个“小任务” left.fork(); right.fork(); } } } public class ForkJoinPoolTest { public static void main(String[] args) throws Exception { ForkJoinPool pool = new ForkJoinPool(); // 提交可分解的PrintTask任务 pool.submit(new PrintTask(0 , 300)); pool.awaitTermination(2, TimeUnit.SECONDS); // 关闭线程池 pool.shutdown(); } }
class CalTask extends RecursiveTask<Integer> { // 每个“小任务”只最多只累加20个数 private static final int THRESHOLD = 20; private int arr[]; private int start; private int end; // 累加从start到end的数组元素 public CalTask(int[] arr , int start, int end) { this.arr = arr; this.start = start; this.end = end; } @Override protected Integer compute() { int sum = 0; // 当end与start之间的差小于THRESHOLD时,开始进行实际累加 if(end - start < THRESHOLD) { for (int i = start ; i < end ; i++ ) { sum += arr[i]; } return sum; } else { // 如果当end与start之间的差大于THRESHOLD时,即要打印的数超过20个 // 将大任务分解成两个小任务。 int middle = (start + end) /2; CalTask left = new CalTask(arr , start, middle); CalTask right = new CalTask(arr , middle, end); // 并行执行两个“小任务” left.fork(); right.fork(); // 把两个“小任务”累加的结果合并起来 return left.join() + right.join(); } } } public class Sum { public static void main(String[] args) throws Exception { int[] arr = new int[100]; Random rand = new Random(); int total = 0; // 初始化100个数字元素 for (int i = 0 , len = arr.length; i < len ; i++ ) { int tmp = rand.nextInt(20); // 对数组元素赋值,并将数组元素的值添加到total总和中。 total += (arr[i] = tmp); } System.out.println(total); ForkJoinPool pool = new ForkJoinPool(); // 提交可分解的CalTask任务 Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length)); System.out.println(future.get()); // 关闭线程池 pool.shutdown(); } }
24.ThreadLocal为线程局部变量,为每一个使用该变量的线程都提供一个变量的副本,每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突
class Account { /* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量 每个线程都会保留该变量的一个副本 */ private ThreadLocal<String> name = new ThreadLocal<>(); // 定义一个初始化name属性的构造器 public Account(String str) { this.name.set(str); // 下面代码用于访问当前线程的name副本的值 System.out.println("---" + this.name.get()); } // name的setter和getter方法 public String getName() { return name.get(); } public void setName(String str) { this.name.set(str); } } class MyTest extends Thread { // 定义一个Account属性 private Account account; public MyTest(Account account, String name) { super(name); this.account = account; } public void run() { // 循环10次 for (int i = 0 ; i < 10 ; i++) { // 当i == 6时输出将账户名替换成当前线程名 if (i == 6) { account.setName(getName()); } // 输出同一个账户的账户名和循环变量 System.out.println(account.getName() + " 账户的i值:" + i); } } } public class ThreadLocalTest { public static void main(String[] args) { // 启动两条线程,两条线程共享同一个Account Account at = new Account("初始名"); /* 虽然两条线程共享同一个账户,即只有一个账户名 但由于账户名是ThreadLocal类型的,所以每条线程 都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条 线程访问同一个账户时看到不同的账户名。 */ new MyTest(at , "线程甲").start(); new MyTest(at , "线程乙").start (); } }
上例只有一个Account对象,但会有三个账户名副本
如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制;如果仅仅要隔离多个线程之间的共享冲突,则可以使用ThreadLocal十七、网络编程
1.三类端口号:公认端口0-1023,紧密绑定一些特定的服务;注册端口1024-49151,松散绑定一些服务,通常是一些应用程序使用;动态或私有端口49152-65535,应用程序使用的动态端口,一般不会主动使用
2.InetAddress类代表IP地址,有Int4Address、Inet6Address两个子类
public class InetAddressTest { public static void main(String[] args) throws Exception { // 根据主机名来获取对应的InetAddress实例 InetAddress ip = InetAddress.getByName("www.crazyit.org"); // 判断是否可达 System.out.println("crazyit是否可达:" + ip.isReachable(2000)); // 获取该InetAddress实例的IP字符串 System.out.println(ip.getHostAddress()); // 根据原始IP地址来获取对应的InetAddress实例 InetAddress local = InetAddress.getByAddress( new byte[]{127,0,0,1}); System.out.println("本机是否可达:" + local.isReachable(5000)); // 获取该InetAddress实例对应的全限定域名 System.out.println(local.getCanonicalHostName()); } }
3.URLDecoder和URLEncoder用于对链接中的字符转码,每个汉字占2个字节,每个字节转换成2个十六进制的数字,每个中文字符将转换成%XX%XX,也与字符集有关
public class URLDecoderTest { public static void main(String[] args) throws Exception { // 将application/x-www-form-urlencoded字符串 // 转换成普通字符串 // 其中的字符串直接从图17.3所示窗口复制过来 String keyWord = URLDecoder.decode( "%B7%E8%BF%F1java", "GBK"); System.out.println(keyWord); // 将普通字符串转换成 // application/x-www-form-urlencoded字符串 String urlStr = URLEncoder.encode( "疯狂Android讲义" , "GBK"); System.out.println(urlStr); } }4.URL类提供了多个构造器用于创建URL对象,URL实例的openConnection方法返回一个URLConnection对象,代表了与URL所引用的远程对象的连接;openStream打开与此URL的链接,并返回一个用于读取该URL资源的InputStream。如果只是发送GET方式请求,则使用connect方法建立和远程资源之间的实际连接即可;如果需要发送POST方式的请求,则需要获取URLConnection实例对应的输出流来发送请求信息
public class GetPostTest { /** * 向指定URL发送GET方法的请求 * @param url 发送请求的URL * @param param 请求参数,格式满足name1=value1&name2=value2的形式。 * @return URL所代表远程资源的响应 */ public static String sendGet(String url , String param) { String result = ""; String urlName = url + "?" + param; try { URL realUrl = new URL(urlName); // 打开和URL之间的连接 URLConnection conn = realUrl.openConnection(); // 设置通用的请求属性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent" , "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); // 建立实际的连接 conn.connect(); // 获取所有响应头字段 Map<String, List<String>> map = conn.getHeaderFields(); // 遍历所有的响应头字段 for (String key : map.keySet()) { System.out.println(key + "--->" + map.get(key)); } try( // 定义BufferedReader输入流来读取URL的响应 BufferedReader in = new BufferedReader( new InputStreamReader(conn.getInputStream() , "utf-8"))) { String line; while ((line = in.readLine())!= null) { result += "\n" + line; } } } catch(Exception e) { System.out.println("发送GET请求出现异常!" + e); e.printStackTrace(); } return result; } /** * 向指定URL发送POST方法的请求 * @param url 发送请求的URL * @param param 请求参数,格式应该满足name1=value1&name2=value2的形式。 * @return URL所代表远程资源的响应 */ public static String sendPost(String url , String param) { String result = ""; try { URL realUrl = new URL(url); // 打开和URL之间的连接 URLConnection conn = realUrl.openConnection(); // 设置通用的请求属性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); // 发送POST请求必须设置如下两行 conn.setDoOutput(true); conn.setDoInput(true); try( // 获取URLConnection对象对应的输出流 PrintWriter out = new PrintWriter(conn.getOutputStream())) { // 发送请求参数 out.print(param); // flush输出流的缓冲 out.flush(); } try( // 定义BufferedReader输入流来读取URL的响应 BufferedReader in = new BufferedReader(new InputStreamReader (conn.getInputStream() , "utf-8"))) { String line; while ((line = in.readLine())!= null) { result += "\n" + line; } } } catch(Exception e) { System.out.println("发送POST请求出现异常!" + e); e.printStackTrace(); } return result; } // 提供主方法,测试发送GET请求和POST请求 public static void main(String args[]) { // 发送GET请求 String s = GetPostTest.sendGet("http://localhost:8888/abc/a.jsp" , null); System.out.println(s); // 发送POST请求 String s1 = GetPostTest.sendPost("http://localhost:8888/abc/login.jsp" , "name=crazyit.org&pass=leegang"); System.out.println(s1); } }5.shutdownInput和shutdownOutput表示输出出具已经发送完毕,socket处于半关闭状态
十八、类加载机制与反射
1.当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、链接、初始化3个步骤来对该类进行初始化。类加载指的是将类的class文件读入内存,并位置创建一个java.lang.Class对象,通常类的加载由系统类加载器完成,也可以通过继承ClassLoader类来创建自己的类加载器。
2.链接阶段负责把类的二进制数据合并到JRE中,包括验证(内部结构是否正确与其他类协调一致)、准备(为静态Field分配内存,并设置默认初始值)和解析(符号引用替换成直接引用)。
3.JVM初始化类步骤:如果这个类还没有被加载和链接,则程序先加载并连接该类;该类的直接父类还没有被初始化,则先初始化其直接父类;如果类中有初始化语句,则系统一次执行这些初始化语句
4.Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口:创建类的实例(使用new、反射以及反序列化创建实例);调用某个类的静态方法;访问某个类或接口的静态Field、或为该静态Field赋值;使用反射方式来强制创建某个类或接口对应的java.lang.Class对象(Class.forName("Person"));初始化某个类的子类;直接使用java.exe运行某个类。对于一个final型的静态Field,如果该Field的值在编译时就可以确定下来,那么这个Field相当于宏变量,编译时直接替换,程序使用该静态Field也不会导致该类的初始化。使用ClassLoader类的loadClass方法来加载某个类时,只是加载该类,并不会执行该类的初始化,使用Class的forName静态方法才会导致强制初始化该类
class Tester { static { System.out.println("Tester类的静态初始化块..."); } } public class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader cl = ClassLoader.getSystemClassLoader(); // 下面语句仅仅是加载Tester类 cl.loadClass("Tester"); System.out.println("系统加载Tester类"); // 下面语句才会初始化Tester类 Class.forName("Tester"); } }5.一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例k1负责加载,则该Person类对应的Class对象在JVM中表示为(Person、pg、k1)。
6.当JVM启动时,会形成由3个类加载器组成的初始类加载器层次结构:Bootstrap ClassLoader根类加载器,负责加载Java的核心类;Extension ClassLoader 扩展类加载器,负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者java.ext.dirs)中JAR包的类;System ClassLoader 系统类加载器,负责在JVM启动时加载来自java命令-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径
7.JVM的类加载机制:全盘负责,加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器载入;父类委托,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类;缓存机制,所有加载过的Class都会被缓存,需要使用某个类时,先从缓存区中搜寻,缓存区不存在该Class对象时,才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。
8.获得Class对象:使用Class类的forName(String clazzName)静态方法;调用某个类的class属性(Person.class);调用某个对象的getClass()方法。第二种方式更安全,性能更好
9.从Class中获取信息
// 使用两个注释修饰该类 @SuppressWarnings(value="unchecked") @Deprecated public class ClassTest { // 为该类定义一个私有的构造器 private ClassTest() { } // 定义一个有参数的构造器 public ClassTest(String name) { System.out.println("执行有参数的构造器"); } // 定义一个无参数的info方法 public void info() { System.out.println("执行无参数的info方法"); } // 定义一个有参数的info方法 public void info(String str) { System.out.println("执行有参数的info方法" + ",其str参数值:" + str); } // 定义一个测试用的内部类 class Inner { } public static void main(String[] args) throws Exception { // 下面代码可以获取ClassTest对应的Class Class<ClassTest> clazz = ClassTest.class; // 获取该Class对象所对应类的全部构造器 Constructor[] ctors = clazz.getDeclaredConstructors(); System.out.println("ClassTest的全部构造器如下:"); for (Constructor c : ctors) { System.out.println(c); } // 获取该Class对象所对应类的全部public构造器 Constructor[] publicCtors = clazz.getConstructors(); System.out.println("ClassTest的全部public构造器如下:"); for (Constructor c : publicCtors) { System.out.println(c); } // 获取该Class对象所对应类的全部public方法 Method[] mtds = clazz.getMethods(); System.out.println("ClassTest的全部public方法如下:"); for (Method md : mtds) { System.out.println(md); } // 获取该Class对象所对应类的指定方法 System.out.println("ClassTest里带一个字符串参数的info方法为:" + clazz.getMethod("info" , String.class)); // 获取该Class对象所对应类的上的全部注释 Annotation[] anns = clazz.getAnnotations(); System.out.println("ClassTest的全部Annotation如下:"); for (Annotation an : anns) { System.out.println(an); } System.out.println("该Class元素上的@SuppressWarnings注释为:" + clazz.getAnnotation(SuppressWarnings.class)); // 获取该Class对象所对应类的全部内部类 Class<?>[] inners = clazz.getDeclaredClasses(); System.out.println("ClassTest的全部内部类如下:"); for (Class c : inners) { System.out.println(c); } // 使用Class.forName方法加载ClassTest的Inner内部类 Class inClazz = Class.forName("ClassTest$Inner"); // 通过getDeclaringClass()访问该类所在的外部类 System.out.println("inClazz对应类的外部类为:" + inClazz.getDeclaringClass()); System.out.println("ClassTest的包为:" + clazz.getPackage()); System.out.println("ClassTest的父类为:" + clazz.getSuperclass()); } }10.创建对象:使用Class对象的newInstance()方法来创建该Class对象对应的实例,这种方式要求该Class对象的对应类有默认构造器;使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例
public class CreateJFrame { public static void main(String[] args) throws Exception { // 获取JFrame对应的Class对象 Class<?> jframeClazz = Class.forName("javax.swing.JFrame"); // 获取JFrame中带一个字符串参数的构造器 Constructor ctor = jframeClazz .getConstructor(String.class); // 调用Constructor的newInstance方法创建对象 Object obj = ctor.newInstance("测试窗口"); // 输出JFrame对象 System.out.println(obj); } }11.通过反射调用方法
import java.util.*; import java.io.*; import java.lang.reflect.*; public class ExtendedObjectPoolFactory { // 定义一个对象池,前面是对象名,后面是实际对象 private Map<String ,Object> objectPool = new HashMap<>(); private Properties config = new Properties(); // 从指定属性文件中初始化Properties对象 public void init(String fileName) { try( FileInputStream fis = new FileInputStream(fileName)) { config.load(fis); } catch (IOException ex) { System.out.println("读取" + fileName + "异常"); } } // 定义一个创建对象的方法, // 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象 private Object createObject(String clazzName) throws InstantiationException , IllegalAccessException , ClassNotFoundException { // 根据字符串来获取对应的Class对象 Class<?> clazz =Class.forName(clazzName); // 使用clazz对应类的默认构造器创建实例 return clazz.newInstance(); } // 该方法根据指定文件来初始化对象池, // 它会根据配置文件来创建对象 public void initPool()throws InstantiationException ,IllegalAccessException , ClassNotFoundException { for (String name : config.stringPropertyNames()) { // 每取出一对key-value对,如果key中不包含百分号(%) // 这个就标明是根据value来创建一个对象 // 调用createObject创建对象,并将对象添加到对象池中 if (!name.contains("%")) { objectPool.put(name , createObject(config.getProperty(name))); } } } // 该方法根据指定文件来初始化对象池, // 它会根据配置文件来创建对象 public void initProperty()throws InvocationTargetException ,IllegalAccessException,NoSuchMethodException { for (String name : config.stringPropertyNames()) { // 每取出一对key-value对,如果key中包含百分号(%) // 即可认为该key是用于为对象的Field设置值, // %前半为对象名字,后半为Field名 // 程序将调用对应的setter方法来为对应Field设置值。 if (name.contains("%")) { // 将配置文件中key按%分割 String[] objAndProp = name.split("%"); // 取出需要设置Field值的目标对象 Object target = getObject(objAndProp[0]); // 该Field对应的setter方法名:set + "属性的首字母大写" + 剩下部分 String mtdName = "set" + objAndProp[1].substring(0 , 1).toUpperCase() + objAndProp[1].substring(1); // 通过target的getClass()获取它实现类所对应的Class对象 Class<?> targetClass = target.getClass(); // 获取该属性对应的setter方法 Method mtd = targetClass.getMethod(mtdName , String.class); // 通过Method的invoke方法执行setter方法, // 将config.getProperty(name)的属性值作为调用setter的方法的实参 mtd.invoke(target , config.getProperty(name)); } } } public Object getObject(String name) { // 从objectPool中取出指定name对应的对象。 return objectPool.get(name); } public static void main(String[] args) throws Exception { ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory(); epf.init("extObj.txt"); epf.initPool(); epf.initProperty(); System.out.println(epf.getObject("a")); } }通过Method的invoke方法来调用对应的方法时,Java会要求程序必须有调用该方法的权限。当确实需要调用某个对象的private方法,可以先调用setAccessible(boolean flag)取消访问权限检查,Constructor和Field都可以调用该方法
12.通过反射访问属性值:
class Person { private String name; private int age; public String toString() { return "Person[name:" + name + " , age:" + age + " ]"; } } public class FieldTest { public static void main(String[] args) throws Exception { // 创建一个Person对象 Person p = new Person(); // 获取Person类对应的Class对象 Class<Person> personClazz = Person.class; // 获取Person的名为name的Field // 使用getDeclaredField,表明可获取各种访问控制符的field Field nameField = personClazz.getDeclaredField("name"); // 设置通过反射访问该Field时取消访问权限检查 nameField.setAccessible(true); // 调用set方法为p对象的name Field设置值 nameField.set(p , "Yeeku.H.Lee"); // 获取Person类名为age的属性 Field ageField = personClazz.getDeclaredField("age"); // 设置通过反射访问该Field时取消访问权限检查 ageField.setAccessible(true); // 调用setInt方法为p对象的age Field设置值 ageField.setInt(p , 30); System.out.println(p); } }引用对象直接用set,而基本数据类型用setXxx(如setInt)
13.操作数组
public class ArrayTest1 { public static void main(String args[]) { try { // 创建一个元素类型为String ,长度为10的数组 Object arr = Array.newInstance(String.class, 10); // 依次为arr数组中index为5、6的元素赋值 Array.set(arr, 5, "疯狂Java讲义"); Array.set(arr, 6, "轻量级Java EE企业应用实战"); // 依次取出arr数组中index为5、6的元素的值 Object book1 = Array.get(arr , 5); Object book2 = Array.get(arr , 6); // 输出arr数组中index为5、6的元素 System.out.println(book1); System.out.println(book2); } catch (Throwable e) { System.err.println(e); } } }
public class ArrayTest2 { public static void main(String args[]) { /* 创建一个三维数组。 根据前面介绍数组时讲的:三维数组也是一维数组, 是数组元素是二维数组的一维数组, 因此可以认为arr是长度为3的一维数组 */ Object arr = Array.newInstance(String.class, 3, 4, 10); // 获取arr数组中index为2的元素,应该是二维数组 Object arrObj = Array.get(arr, 2); // 使用Array为二维数组的数组元素赋值。 // 二维数组的数组元素是一维数组,所以传入Array的set()方法的 // 第三个参数是一维数组。 Array.set(arrObj , 2 , new String[] { "疯狂Java讲义", "轻量级Java EE企业应用实战" }); // 获取arrObj数组中index为3的元素,应该是一维数组。 Object anArr = Array.get(arrObj, 3); Array.set(anArr , 8 , "疯狂Android讲义"); // 将arr强制类型转换为三维数组 String[][][] cast = (String[][][])arr; // 获取cast三维数组中指定元素的值 System.out.println(cast[2][3][8]); System.out.println(cast[2][2][0]); System.out.println(cast[2][2][1]); } }14.Proxy提供了用于创建动态代理类和代理对象的静态方法,它也是所有动态代理类的父类
15.使用反射来获取泛型信息:
public class GenericTest { private Map<String , Integer> score; public static void main(String[] args) throws Exception { Class<GenericTest> clazz = GenericTest.class; Field f = clazz.getDeclaredField("score"); // 直接使用getType()取出Field类型只对普通类型的Field有效 Class<?> a = f.getType(); // 下面将看到仅输出java.util.Map System.out.println("score的类型是:" + a); // 获得Field实例f的泛型类型 Type gType = f.getGenericType(); // 如果gType类型是ParameterizedType对象 if(gType instanceof ParameterizedType) { // 强制类型转换 ParameterizedType pType = (ParameterizedType)gType; // 获取原始类型 Type rType = pType.getRawType(); System.out.println("原始类型是:" + rType); // 取得泛型类型的泛型参数 Type[] tArgs = pType.getActualTypeArguments(); System.out.println("泛型类型是:"); for (int i = 0; i < tArgs.length; i++) { System.out.println("第" + i + "个泛型类型是:" + tArgs[i]); } } else { System.out.println("获取泛型类型出错!"); } } }