《Thinking In Java》阅读笔记
前四章:对象导论、 一切都是对象、 操作符、 控制执行流程
public在一个文件中只能有一个,可以是一个类class或者一个接口interface >一旦创建一个引用,就希望它能与一个新的对象相关联: String s = "hello"; String s = new String("hello"); s:遥控器(引用) “hello”:电视机(对象) 数据存储在: 寄存器:最快的存储区,在处理器内部 堆栈:RAM(随机访问存储器),栈底在上栈顶在下,指针向下移动->分配新的内存 堆:一种通用的内存池(RAM),用于存放所有Java对象,当 new 一个对象,代码执行到的时候自动在堆里进行存储分配 常量存储:通常直接存放在程序代码内部 非RAM存储:如果数据完全存活与程序之外,那它可以不受程序的任何控制,在程序没有运行时也可以存在。例:流对象,持久化对象 记住: new 将对象存储在“堆”中 基本类型:new不是很有效,不用new创建,非引用的“自动”变量,直接存储“值”于栈内 Java 基本数据类型占用存储空间大小不受机器限制 所有数值类型都有正负号,没有无符号数 byte 1 char 2 short 2 int 4 long 8 float 4 double 8 boolean 所占的存储空间没有明确指定,仅定义为能够取字面值 true 和 false 高精度数字: BigInteger 支持任意精度的整数 BigDecimal 支持任何精度的定点数 引用的空值:null Java 的垃圾回收器存在,使得我们不需要在用完某一对象后进行手动销毁(释放内存) 类中定义的成员是基本类型会自动初始化为 0 boolean值初始化false 方法中定义的基本变量要手动进行初始化,不然会报:变量没有初始化的 错误 int x; System.out.println(x);//报错:x没有初始化 字符串的length()方法返回的是字符串中字符个数 定义包名(指定名字空间):使用反转的域名,全部小写 import用于导入类库 执行 new 来创建对象时,数据才存储空间才被分配,其方法才供外界调用 static设计的目的: 1、只想为某特定域分配单一存储空间,而不去考虑究竟要创建多少对象,甚至根本不创建对象 2、希望某个方法不与包含它的类的任何对象关联在一起,没有创建对象也可以使用此方法 记住:static是类成员 调用非类方法(没有static修饰)必须使用此类的某一对象 javadoc的使用(注解): 第一行: //:文件路径/文件名 @author @version @classname @description 用于方法文档: @param 参数列表中的标识符 @return 返回值含义 @throws 在别处定义的异常的无歧义名字 @deprecated //表示方法过时,可能在后续版本中废弃 最后一行: ///:~ Java 编码风格:驼峰风格 类名全部单词首字母大写 方法、字段、对象引用名、变量名 第一个单词首字母小写,其他单词首字母大写 操作符中一元减号起到转变数据的符号的作用: -a 就是a的相反数 与、或、非 逻辑运算符 只适用于布尔值 不会像C一样适用于数值 !1 是非法的 java中增加了无符号右移操作符 >>> 空位全部补0 数字的二进制表示形式称为:有符号的二进制补码 int x = 1, y = 2, z = 3; 是合法的 char byte short 在使用算术运算符中数据类型被提升为int 得到的结果是int类型 foreach语句中迭代变量仅在for存活 多个foreach语句内部的迭代变量完全可以相同 for(int i: iArray){ System.out.println(i); } 在Java里需要使用标签的唯一理由就是因为循环嵌套存在,而且想从多层嵌套中break或continue break label; continue label;
初始化与清理
构造器(constructor) 调用构造器是编译器的责任 构造器没有返回值,这与返回值为void完全不同的 java中,“初始化”和“创建”捆绑在一起,两者不能分离 方法重载:在一个类内可以定义多个含有不同参数类型列表的同名的方法来实现方法的重载 构造器是典型的例子 默认构造器在类内没有明确指定一个构造器时编译器会自动添加,如果自己提供了构造器编译器就不会在自作多情 this 表示对“调用方法的那个对象”的引用 方法内部调用同一类的另一个方法,不必使用 this 直接调用即可 同一类内成员相互使用时,不必加this 只有当要明确指出对当前对象的引用时才使用this this可以用来调用一个构造器,理解同与super 在构造器中调用构造器 除了构造器,其他地方都不可以调用构造器 static 方法就是没有 this 的方法 static内部不能直接调用非静态方法,反之可以 在代码中出现大量的static方法,就该重新考虑自己的设计了 清理:终极处理和垃圾回收 1、对象可能不被垃圾回收 2、垃圾回收并不等于“析构” 3、垃圾回收只与内存有关 当垃圾回收时调用finalize()方法,finalize()方法不是进行普通的清理工作的合适场所 记住:无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机(JVM)并没有面临内存耗尽的情形 它是不会浪费时间去执行垃圾回收以恢复内存的 垃圾回收器对于提高对象的创建速度具有明显的效果 “自适应的、分代的、停止-复制、标记-清扫”式垃圾回收器 成员初始化: 类内成员变量初始化默认值为 0 (引用是null) 记住:构造器进行的类成员变量初始化无法阻止自动初始化的进行,它在构造器被调用之前发生 在类的内部,变量定义的先后顺序决定了初始化的顺序,但所有的初始化都优先与构造器内的初始化 静态(static)初始化仅执行一次,静态初始化只在class对象首次加载的时候进行一次 显示的静态初始化: public class Spoon{ static int i; //静态初始化块 static{ i = 1; } } 数组初始化 int[] intArray = {1, 2, 3, }; int[] intArray = new int[]{1, 2, 3, }; int[] intArray = new int[3];//默认初始化为0 可变参数列表 public static void main(String[] args){} public void print(Object... args){} //其中Object可以为具体的类型 args仍旧是一个数组 调用print: print(1, 2, "hello"); print((Object[])new Integer[]{1, 2, 3}); 对一个类的实例直接打印:得到 实例名@地址 枚举类型 enum public enum Months{ JANUARY, FEBRUARY, } 可以与switch完美契合 enum类有 toString()、ordinal()(获取某个特定enmu常量的声明顺序)、static values()(存贮全部enum值的一个数组) for(Months month: Months.values){ System.out.println(month + "," + month.ordinal); }
访问权限控制
包 -> 库单元 |package| |... |-package| |... |.java java可运行程序是一组可打包并压缩为一个Java文档文件(Jar包)的.class文件 机器上要配置环境变量 CLASSPATH java解析器可以将报名 com.package1.xlc 中的 . 替换为 / 来构成一个路径名称 对于jar包,路径要包括jar包的具体名 : com/package1/xlc/myjar.jar 导入类内静态方法: import static com.package1.xlc.MyClass.*; 各访问修饰词: 默认:包访问权限 private:你无法访问 protected:继承访问权限 public:接口访问权限 class Sundae{ private Sundae(){} static Sundae makeASundae(){ return new Sundae(); } } 这个类是无法被继承的,而且无法通过 new Sundae();来创建一个对象 可以使用 Sundae sundae = Sundae.makeASundae(); 类访问权限只有两种选择: 接口访问权限(public) 包访问权限(默认、不加修饰) 应用实例: >无法直接创建类的对象、不能用于继承 class Class1{ private Class1(){} public static Class1 makeAClass1(){ return new Class1(); } } >无法直接创建类的对象、只能创建一个实例 class Class2{ private Class2(){} private static Class2 class2 = new Class2();//定义一个Class2类型的私有静态成员并初始化为Class2的一个实例 public static Class2 returnAClass2(){ return class2; } } 记住:相同目录下的所有不具有明确package声明的文件,都被视作是该目录下默认包的一部分
复用类
每一个非基本类型的对象都有一个 toString() 方法,可以在类内重写它来满足自己的需求 java的OOP:当创建一个类时,总是在继承,除非已经明确指出要从其他类中继承,否则就是在隐式地从java的标准根类 Object 进行继承 在多个类中可以都含有 main() 方法,当命令行 java 类名 此类的main会被调用 一个有用的规则:为了继承,一般的规则是将所有的数据成员都指定为private,所有的方法都指定为public super.fatherMethod() 调用父类的方法 子类的对象在 new 的时候 构建过程是从基类“向外”扩散的,基类在导出类构造器可以访问它之前,已经完成初始化 class Father{ Father(int i){ System.out.println("Father Constructor"); } } class Son extends Father{ Son(int i){ //没有默认的基类构造器,或者想调用一个带参数的基类构造器,必须用关键字 super 显示的调用,要放在子类构造器的首行 super(i); System.out.println("Son Constructor"); } } 代理:将一个成员对象置于所要构造的类中 public class AObject{ private String name; private AObject aobject = new AObject(); public AObject(String name){ this.name = name; } public void aMethod(int i){ aobject.aMethod(i); } } 确保正确清理:首先,执行类的所有特定的清理工作,其顺序同生成顺序相反;然后调用基类的清理方法 is a 是继承 has a 是组合 向上转型:子类实例转型为父类实例 (较专用类型转较通用类型) 常用的: 直接将数据和方法包装进一个类,并使用该类的对象 运用组合技术使用现有类来开发新的类 继承技术其实不太常用,到底该不该用继承的一个问题:需要从新类向基类进行向上转型么? final: 命名,使用全大写形式,多个单词采用下划线 一个永不改变的编译时常量 一个在运行时被初始化的值,但是不希望它被改变 final修饰的基本类型值无法改变,修饰一个引用则这个引用的值无法改变,但是指向的对象可能会变 static final 只占据一段不能改变的存储空间(强调一个、不变) final变量必须在使用前进行初始化,此时出现空白final 一个类中的final域可以做到根据对象而有所不同,却又保持其恒定不变的特性: class Aaa{ private final int i; //每次实例化的时候都可以指定i的值,一旦指定后就无法在修改 Aaa(int i){ this.i = i; } } final参数: void f(final int i){ i++;//错误 i不可以被改变 } void g(final int i){ System.out.println(i);//可以取值 } final方法:确保在继承中使用方法行为保持不变,并且不会被覆盖 继承的时候,“覆盖”只有在某方法是基类的 接口 的一部分时才会出现,private不是基类的接口的一部分 基类中有一个private方法aMethod,导出类继承与此基类并且也有一个aMethod的方法,这里只是新创建一个方法,不存在Override问题 private 修饰的作用:隐藏 finale修饰一个类:目的在于对该类的设计永不需要做任何改动,或者出于安全的考虑,不希望它有子类(不能被继承) 继承与初始化: 在一个类上运行Java,第一件事就是试图访问此类的 main() 方法 加载器开始启动并找出此类的编译代码(在名为 .class文件内) 在对其加载的过程中,遇见 extends 关键字,找出其基类,继续加载基类,基类还有基类以此类推 接下来,根基类中的static开始初始化,然后是其导出类的static,以此类推 现在对象可以被创建了, 首先对象中所有基本类型初始化为0,引用初始化为null 然后基类的构造器被调用,导出类构造器和基类构造器一样,以相同的顺序经历相同的过程 构造器完成之后,实例变量按其次序被初始化,最后构造器的其余部分被执行
多态
java中除了static方法和final方法(static方法属于final方法)是前期绑定之外,其他所有方法都是后期绑定 一种工厂: public class Factory{ private Random rand = new Random(10); public Father next(){ switch(rand.nextInt(3)){ default: case 0: return new Son1(); case 1: return new Son2(); case 3: return new Son3(); } } } 多态的体现: Father的三个导出类Son1、Son2、Son3都重写了Father的 aMethod()方法 Father[] fathers = new Father[]{new Son1(), new Son2(), new Son3(),}; for(Father father: fathers){ father.aMathod(); } 由于动态绑定,可以成功的调用各类实例自己的aMethod()方法 不想让导出类覆盖的方法,指定为private: public class PrivateOverride{ private void f(){ System.out.println("private f()"); } public static void main(String[] args){ PrivateOverride po = new Derived(); po.f(); } } public class Derived extends PrivateOverride{ public void f(){ System.out.println("public f()"); } } 输出结果是:private f() //基类中的f()方法无法被覆盖,向上转型后调用po的f()方法是基类的 基类中已经有的成员,导出类再次定义则不会覆盖,它们各自有自己的存储空间导出类中要获得基类对象的属性应显示的使用 super.value 构造器是static方法,static的声明是隐式的 初始化顺序: 0、在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的 0 1、调用基类的构造器,反复递归,首先构造这种层次结构的根,然后是下一层导出类,直到最底层的导出类 2、按声明顺序调用成员的初始化方法 3、调用导出类构造器的主体 如果要手动进行清理工作,则销毁顺序与初始化顺序相反,导出类先进行清理然后才是基类,以为导出类的清理工作可能需要基类中的某些方法 编写构造器的一条有效准则:用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法 一条通用准则:用继承表达行为间的差异,用字段表达状态上的变化 class Actor{ public void act(){} } //通过继承表达 act 的不同 class HappyActor extends Actor{ public void act(){ System.out.println("HappyActor"); } } class SadActor extends Actor{ public void act(){ System.out.println("SadActor"); } } //使用组合使自己状态发生变化 class Stage{ private Actor actor = new HappyActor(); //状态发生变化 public void change(){ actor = new SadActor(); } public void performPlay(){ actor.act(); } } 抽象类: 关键字 abstract 抽象方法:仅声明,没有方法体 public abstract void abMethod(); 包含抽象方法的类叫做抽象类: abstract class AbClass{ private i; public abstract void abMethod(); public String method(){ return "hello world"; } } 如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中 所有 抽象方法提供方法定义,如果不这样则导出类也是抽象类
接口
关键词 interface 可以认为是一种特殊的类,但是其支持多引用(implements) 像类一样,可以在定义时 interface前加public(但是仅限在与其同名的文件内),不加public则此接口只具有报访问权限 接口内也可以包含域,但是这些域隐式的是static和final的 可以显示的声明接口内的方法为public 如果不这样,它们也是public的 interface Interface{ int value = 1;//static && final void method1(); String returnString(Stirng str); } 一个类可以实现多接口:必须要实现两个接口的所有方法,如果不则应将Class定义为abstract class Class implements Interface1, Interface2{ } 向上转型时,只可使用转型的那个接口内还有的方法 接口可继承: 实现扩展接口 interface Interface2 implements Interface1, Interface{ void method(); } 适配: interface Interface{ Object method(Object value);//此方法接收任何类型,并支持返回任何类型 } 接口可以嵌套在一个类或接口中 设计中,恰当的原则应该是优先选择类而不是接口,从类开始,如果接口的必需性变得非常明确,那么就进行 重构 工厂型设计的一个例子: interface Car{ void setname(String str); } interface CarFactory{ Car getCar(); } class Car1 implements Car{ private String name; public void setname(String name){ this.name = name; } } class Car2 implements Car{ private String name; public void setname(String name) { this.name = name; } } class Car1Factory implements CarFactory{ public Car getCar() { return (new Car1()); } } class Car2Factory implements CarFactory{ public Car getCar() { return (new Car2()); } }
内部类
成员内部类: 当生成一个内部类的对象时,此对象与制造它的外围对象之间有一种联系,它不需要任何特殊条件就可以访问其外围对象的所有成员,如同它拥有外部的成员一样 在内部类中生成对外部类对象的引用以及通过外部类对象创建内部类的对象: public class Outer{ void f(){ System.out.println("outer.f()"); } public class Inner{ public Outer outer(){ //在内部类中生成对外部类对象的引用 return Outer.this; } } public static void main(String[] args){ //通过外部类对象创建内部类的对象 Outer.Inner outerInner = new Outer().new Inner(); outerInner.outer.f(); } } 记住:成员内部类对象的创建依赖与外部类的 对象 ,没有外部类 对象 之前是不可能创建内部类 对象 的 成员内部类是private的,相同外部类内的非private成员内部类是无法访问的,只有外部类有其访问权限 局部内部类: 定义在某方法内部的类 / 定义在方法内的某作用域内 有效范围限制在这个域内 匿名内部类: 创建一个而继承于基类的匿名类的对象(涵盖的信息:它是一个子类,创建其对象时进行向上转型) 帮助理解:看起来似乎是你正要创建一个对象,但是然后你却说:“等一等,我想在这里插入一个类的定义” interface Interface{} public class Class{ public Interface itf(){ return new Interface(){ private int i = 0; public int value(){ return i; } };//分号用来标记表达式的结束 } } ? 如果定义一个匿名内部类,并希望它使用一个其外部定义的对象,编译器会要求其参数引用是 final 的(实验中并没有) 匿名内部类没有构造器:因为它没有名字! <<通过实例初始化可达到为匿名内部类创建一个构造器的效果>> 匿名内部类可以扩展接口和类,但是不能两者兼容,对于接口也只能实现一个 嵌套类: 声明为static的内部类是嵌套类,这样的内部类对象与其外部类对象之间没有联系 嵌套类意味着: 要创建嵌套类的对象,并不需要其外围类的对象 不能从嵌套类的对象中访问非静态类的外围类的对象 普通内部类不能有static数据和static字段,也不能包含嵌套类,但是嵌套类可以包含所有这些东西 接口内部可以定义类:默认是 public 和 static 的 不懂:闭包 回调 一个内部类不管嵌套多少层,都可以透明地访问所有它嵌套入的外围类的所有成员: class MNA{ private void f(){} class A{ private void g(){} public class B{ void h(){ g(); f(); } } } } 内部类的继承: class WithInner{ class Inner{} } public class InheritInner extends WithInner.Inner{ //当要生成一个构造器时,必须使用这样的语法 InheritInner(WithInner wi){ wi.super(); //!!!!!!!!!!! } public static void main(String... args){ WithInner wi = new WithInner(); InheritInner ii = new InheritInner(wi); } } 内部类可以被覆盖么: 当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化,这两个内部类是完全独立的两个实体,各自在自己的命名空间内 外围类继承某一类后,其内部类可以明确指定继承于被继承的类内的一个类,此时可以覆盖内部类对方法 局部内部类与匿名内部类的选择: 如果需要一个已命名的构造器,或者需要重载构造器,就需要使用局部内部类,此时匿名内部类是无法实现的(没有名字),它只能用于实例初始化 还有就是一个局部内部类可以创建多个其对象,匿名内部类只会创建一个 内部类标识符: 每一个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息 内部类class文件命名:外围类名字+$+内部类名字.class 如果是匿名内部类,则产生一个数字作为其标识符
持有对象
Java容器类类库的用途是“保存对象”,划分的两个概念: Collection Map 泛型:一个例子 List<String> list = new ArrayList<String>();//指定list的存储类型为String,实例对象后向上转型为List接口,此时list可用的方法都是List接口中含有的 添加一组数据: Arrays.asList() 接受一个数组,或一个用逗号分割的元素列表,转化为List对象,但是其底层是数组,不能调整大小 Arrays.asList(1, 2, 3, 4); Integer[] intArray = {1, 2, 3, 4}; Arrays.asList(intArray); Collections.addAll() 接受一个Collection对象,以及一个数组或是用逗号分割的列表 Collection<Integer> collection = new ArrayList<Integer>(Arrays.asList(1, 2, 3));//Collection的构造器接受一个Collection用于自己的初始化 Collections.addAll(collection, intArray); Collections.addAll(collection, 1, 2, 3); collection.addAll(Arrays.asList(intArray));//此方法只能接受一个Collection对象作为参数 创建一个空的Collection,然后使用对象的addAll()方法运行速度快 数组转List的时候:Arrays.<MyType>asList(new MyType(1), new MyType(2)); 容器的打印很规范: List:[hello, world] Set:[hello, world] Map:{nihao=hello, shijie=world} Set: HashSet:元素存储顺序没有意义,很快的查找速度 TreeSet:按照比较结果的升序保存对象 LinkedHashSet:按照被添加的顺序保存对象 Map: HashMap:保存顺序不是插入顺序,基于key的某种算法存储,很快的查找速度 TreeMap:按照比较结果的升序保存键(key) LinkedHashMap:按照插入顺序保存键,并保留了HashMap的查询速度 List: ArrayList 易于随机访问元素,但是在LIst的中间插入和移除元素时较慢 LinkedList 在List中间进行插入和移除代价会较低,提供了优化的顺序访问,但是随机访问会相对比较慢 一堆方法,用时查阅--224页 两者都是按照插入顺序保存元素,LinkedList包含更多的操作、 迭代器:Iterator 只能向前移动 使用iterator()方法返此容器的迭代器:Iterator<MyType> it = myArrayList.iterator(); 通过hasNext()方法检查是否还有元素:it.hasNext() 通过next取出当前值,并将指针向后移动:it.next() 通过remove删除容器中元素: it.next(); it.remove(); 迭代器统一了对容器的访问方式: public void display(Iterator<Object> it){ if(it.hasNext()){ System.out.println(it.next());//取出当前值后指针自动后移 } } ListIterator: 只适用于各种List类的访问,可以双向移动 创建List的专属迭代器:ListIterator<MyType> it = myArrayList.listIterator();//从0开始 ListIterator<MyType> it = myArrayList.listIterator(3);//指定从index=3开始 next仍然适用于取出当前值,并指针前移:it.next() 当前指针位置:it.nextIndex() 当前指针前一个位置的索引:it.previousIndex() 使用set修改当前指针位置的元素:it.set(new MyType()) LinkedList: 它添加了作用于栈、队列和双端队列的方法 取出第一个元素但不移除: getFirst();//List为空抛出异常 element();//与getFirst完全一样 peek();//List为空返回null 取出第一个元素并移除: removeFirst();//List为空抛出异常 remove();//与removeFirst完全一样 poll();//List为空返回null 添加一个元素: addFirst();//在表头添加 add();//在表尾添加 addLast();//表尾添加 offer();//表尾添加 移除并返回最后一个元素: removeLast();//List为空抛出异常? 栈(Stack):后进先出 采用LinkedList构建栈 public class Stack<T>{//重点:使用泛型,<T>告诉编译器这是一个参数化类型 构建此类的对象时可以采用泛型指定类型 private LinkedList<T> storage = new LinkedList<T>(); //增 public void push(T v){ storage.addFirst(v); } //查 public T peek(){ storage.getFirst(); } //删 public T pop(){ storage.removeFirst(); } //判空 public boolean empty(){ return storage.empty(); } //转串 public String toString(){ return storage.toString(); } } Set: HashSet Set最常被使用的是测试归属性:contains()方法 查找是Set中最重要的操作就是查找:基于对象的值 实际上Set就是Collection,只是行为不同 HashSet内存储的元素没有顺序:出于速度考虑使用了散列 TreeSet将元素存储在红-黑树数据结构中,而HashSet使用的是散列函数,LinkedHashSet也使用了散列,但是看起来它使用了链表来维护元素的插入顺序 TreeSet可以根据对象的值在其插入的时候进行排序 Map: <key, value> 查看是否包含某个key或value:containsKey() containsValue() Map的value可以其他容器:例如Map<ClassName, List<MyType>> Map可以返回他的键的Set:myMap.keySet() myMap.values()//返回值类型是Collection 之前没见过:List<? extends MyType> //此处的泛型是所有继承与MyType的类 Queue: 队列常被当作一种可靠的将对象从程序的某一个区域传输到另外一个区域的途径 彩蛋:char的修饰类 Character PriorityQueue: 默认最小值拥有最高的优先级 可以在构造队列的时候调用含参构造器指定Comparator设定元素优先级 PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>(3, Collections.reverseOrder()) Collection 和 Iterator: 实现Collection可以轻松使用foreach进行遍历 生成Iterator是将队列与消费队列的方法连接在一起的耦合度最小的方式,并与实现Collection相比,它在序列类上施加的约束也少的多 ?? Iterable接口: 该接口包含一个能够产生Iterator的iterator()方法(匿名内部类实现) 并且Iterable接口被foreach用来在序列中移动,任何实现了Iterable的类,都可以将他用于foreach语句中 大量的类都是Iterable类型,主要包括有Collection类(除Map) foreach可以用于数组,但是数组不能自动包装成Iterable:直接作为Iterable类型传入会报异常 <T> void test(Iterable<T> ib){//指定泛型的方法 要在方法返回之前加 <T> for(T t: ib){ System.out.println(t); } } String[] strings = {"A", "B"}; test(strings);//!!!!!!!!!!失败 new 出来的容器会重新给其要存储的元素分配内存,如果将一个List作为参数传入则就是生成了一个副本,对其操作不会影响之前的内容 要进行大量随机访问就使用ArrayList,如果要经常从表中间插入或删除元素则使用LinkedList 各种Queue和Stack的行为由LinkedList提供支持 各种不同的容器类的方法:246页
异常
当抛出异常后,有几件事会随之发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了) 被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。 这个恰当的地方就是异常处理程序,它的任务就是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去 抛出一个异常: throw new NullPointerException(); 所有标准异常类都有两个构造器:一个是默认构造器,一个是接受字符串作为参数来把相关信息放入异常对象的构造器 异常类型的根类:Throwable 捕获异常: 监控区域:一段可能产生异常的代码,并且后面跟着处理这些异常的代码 如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),这个方法将在抛出异常的过程中结束 try{}catch(Type1 id1){} 终止:出现异常直接停止执行 恢复:出现异常,尝试修复在继续执行 一种方式是try放在while循环里 自定义异常继承于与其意思相近的异常(很难找),可以继承于Exception 输出错误信息到控制台可以采用System.err.println();将错误发送给标准错误流 自定义异常并创建自己的构造器: class MyException extends Exception{//一定要继承于基类异常 如Exception MyException(){} MyException(String msg){ super(msg);//直接调用父类构造器 } } 在捕获异常后打印栈轨迹: try{ }catch(Exception e){ //可以指定将信息发送的输出流,比如System.out标准输出流 默认是发送到标准错误流System.err e.printStackTrace(); } ???使用java.util.logging工具将输出记录到日志 Throwable类中的getMessage()方法可以获得异常的详细信息,所有异常也都会有此方法 捕获所有异常: 使用异常类型的基类Exception catch(Exception e){ System.out.println("Caught an exception"); } 可以调用其从Throwable继承来的: //获取详细信息 String getMessage(); String getLocalizedMessage(); String toString(); //打印栈轨迹 void printStackTrace(); void printStackTrace(PrintStream); void printStackTrace(java.io.PrintStream); Throwable fillInStackTrace(); 用于在Throwable对象的内部记录栈帧的当前状态 ---->更新异常 Object类中的 getClass() 可以返回一个表示此对象类型的对象: 再 getName() getSimpleName() printStackTrace()方法所提供的信息可以通过getStackTrace()方法直接访问: try{ throw new Exception(); }catch(Exception e){ for(StackTraceElement ste: e.getStackTrace()){ //实际上还可以打印整个StackTraceElement 包含其他附件的信息 System.out.println(ste.getMethodName()); } } 一个被抛出的异常被捕获后,printStackTrace()方法显示的仍是原来异常抛出点的 调用栈 信息,而并非重新抛出点的信息 但是可以在捕获后调用异常的fillInStackTrace()方法,会将当前的调用栈信息填入原来那个异常对象,此行代码成了异常的新发地 在捕获到一个异常后可以加以包装再次抛出或处理,或重新抛出一个新的异常,而前一个异常对象由于是new在堆上创建的,垃圾回收器会自动把它们清理掉 异常链: 在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来 Trowable的子类在构造器中都可以接受一个 cause 对象作为参数,cause表示原始异常 只有三个基本异常提供了带 cause 参数的构造器:Error、Exception、RuntimeException 其他的异常需要使用 initCause()方法而不是构造器: MyException myException = new MyException(); myException.initCause(new NullPointerException());//创建新异常并将旧异常包装进来 catch(RuntimeException e){ throw new Exception(e);//自带接受cause的构造器 } Throwable类被用来表示任何可以作为异常被抛出的类: Error:表示编译时和系统错误 Exception:可以被抛出的基本类型 特例异常:RuntimeException 它们会自动被Java虚拟机抛出,所以不必在异常说明中把它们列出来: void test1() extends Exception{//注意此处一定要有异常说明 throw new Exception(); } void test2(){//此处不需要异常说明,其输出被报告给System.err throw new NullPointerException(); } 记住:只能在代码中忽略RuntimeException类型的异常,其他类型异常的处理都是由编译器强制实施的,原因:RuntimeException代表的是编程错误 使用finally进行清理:不管try内的代码抛不抛出异常,finally块的代码都会执行 记住:finally子句无论如何都会被执行 什么时候使用finally:当要把除内存之外的资源恢复到它们的初始状态时,就会使用finally,例如:关闭已经打开的文件或网络连接等等 在return中使用finally: public class Return{ public static void f(int i){ try{ if(i == 1){return;} }finally{//此处的代码仍然会被执行 System.out.println("print in finally"); } } public static void main(String[] args){ f(1);//此时仍然会输出 print in finally } } 异常丢失: try{ throw new Exception("an exception"); }finally{ //抛出的异常没有被处理和显示,就好像没有出现异常一样 return; } 异常的限制:需要记住的几点 1、基类方法声明异常,派生类重写此方法可以不声明异常或只声明基类的异常 2、基类方法不声明异常,派生类重写的方法不可声明异常 3、接口不能添加异常对于基类中已经存在的方法:接口与基类含有相同方法 4、构造器可以声明新的异常,但派生类的构造器一定要声明基类构造器的异常 5、处理某一类对象,编译器就会强制要求捕获这个类所抛出的异常 6、对于某派生类将其向上转型,编译器就会要求捕获基类的异常 7、派生类中的方法可以声明基类方法声明的异常的派生异常 8、派生类构造器不能捕获基类构造器抛出的异常 总结:基类的能力强于接口,由于存在向上转型,基类声明的异常派生类可以不声明,但不能增加声明新的异常(包括接口中的) 在创建需要清理的对象之后,立即进入try-finally语句块 try{ //需要执行的任务 }finally{ //后续的清理工作 } 匹配异常:catch会按照代码的书写顺序进行匹配捕获,捕获到立即处理后续不再继续查找 捕获的某个异常可以通过 getCause()方法获得异常信息并且可以直接作为异常被再次抛出: catch(RuntimeException re){ throw re.getCause(); } 技巧:在为了简化程序时,可以将异常包装成RuntimeException,因为此异常是不需要异常说明的
字符串
String对象是不可变的,每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象来包含修改后的字符串内容 每当把String对象作为方法的参数被传入时,都会复制一份引用 对于方法而言,参数是为该方法提供信息的,而并不是想让该方法改变自己的 String对象通过 += 拼接的本质: 编译器自动引用java.lang.StringBuilder类,通过现有的String对象值生成Stringbuilder对象,然后在调用append()方法将字符串拼接,再调用toString()方法得到新的String对象 无意识的递归: public class InfiniteRecursion{ public String toString(){ return "InfiniteRecursion address:" + this + "\n"; //此处会将尝试将this转为String,那就要调用toString方法形成了递归调用 } public static void main(String[] args){ List<InfiniteRecursion> v = new ArrayList<InfiniteRecursion>(); for(int i = 0; i < 10; ++i){ v.add(new InfiniteRecursion); } System.out.println(v); } } 正确方式:调用Object.toString()方法:使用 super.toString()//因为它肯定是一个Object类 String类的方法都会返回一个新的String对象,如果内容没有发生改变则只是返回指向原对象的引用 格式化输出://参数的格式书写与C完全一样,包括宽度、左对齐还是右对齐以及精度等等 System.out.format(); System.out.printf(); Formatter类://java.util.Formatter public class PrintFormat{ private Formatter f = new Formatter(System.out);//参数告诉它最终结果向哪输出,常用PrintStream、OutputStream、File public void printF(){ f.format("%-15s %5s %10.2f\n", "Tom", "", 3.14);//此时调用printF方法就会将格式化字符串输出在System.out } } String.format()方法:String类的static方法,接受与Formatter.format()同样的参数,返回一个格式化后的String对象 Scanner类:控制太输入 Scanner stdin = new Scanner(System.in);//接受任何类型的输入对象 示例具有所有类型(除char)对应的next方法,以及hasNext方法用来判断下一个输入分词是不是所需的类型 Scanner有一个假设,在输入结束时会抛出IOException,所以Scanner会把IOException吞掉,可以通过ioException()方法找到最近发生异常(这应该不会用到) Scanner可以通过useDelimiter()方法指定定界符(就是用啥切分),默认是空白字符: Scanner scanner = new Scanner("12, 13, 14, 15, 16"); scanner.useDelimiter("\\s*,\\s*");//指定逗号(包括逗号前后任意的空白字符)作为定界符
类型信息
Java如何让我们在运行时识别对象和类的信息? 1、传统的RTTI,它假定我们在编译时就已经知道了所有的类型(Run-Time Type Information) 在运行时,识别一个对象的类型 2、“反射”机制,它允许我们在运行时发现和使用类的信息 “多态”是面向对象编程的基本目标 Class对象: 每个类都有一个Class对象(被保存在.class文件) 类型信息如何表示:这项工作是由称为Class对象的[特殊对象]完成的,它包含了与类有关的信息 事实上,Class对象就是用来创建类的所有的“常规”对象的 !!如果不好理解可以类比Object类,也就是会有一个Class类它就存在那里,当创建一个类时就会得到此类的Class对象并保存在相应的.class文件中 重点:所有的类都是在对其第一次使用时,动态加载到JVM的。 当程序创建第一个对类的静态成员的引用时,就会加载这个类。 构造器也是类的静态方法,即使在构造器之前并没有使用static关键字 使用new操作符创建类的新对象也会被当作对类的静态成员的引用(含义:new时候也会加载类到JVM) Java程序在它开始运行之前并非被完全加载,其各个部分是在必须时才加载的 类加载器首先检查这个类的Class对象是否已经加载,如果没有,默认的类加载器就会根据类名查找.class文件; 在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码 一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象 静态块: class Test{ static {System.out.println("hello");}//没有方法体 } Class类的众多方法: forName("package.ClassName");//取得Class对象的引用,传进去的是一个具体的类名,得到一个其相应的Class对象的引用 如果已经有ClassName类的对象,还可以使用Object的方法getClass()获取一个Class对象的引用 getName();//产生全限定的类名(包括包名) getSimpleName();//产生不包含包名的类名 getCanonical();//产生全限定的类名(包括包名) isInterface();//判断这个Class对象是否表示某个接口,接口.isInterface();才会返回true getInterface();//返回Class对象,它们表示某个Class对象中所包含的接口 getSuperclass();//查找某Class对象的直接基类,返回的是基类的Class对象 newInstance();//不知道确切的类型,仍旧创建那种类型的实例对象,可以用一个Object的引用来接收 使用此方法来创建实例的类必须带有[默认的构造器] isInstance();//接受一个实例对象,判断其是否是此类的一个实例 类字面常量: 另一种方法来生成对Class对象的引用 可以应用于普通的类,也可以应用于接口、数组以及基本数据类型 基本数据类型的包装类,还存在一个TYPE字段(一个引用),指向基本数据类型的Class对象:char.class(建议使用) <=> Character.TYPE 为了使用类要做的准备工作: 1、加载 由类加载器执行 2、链接 验证类中大的字节码,为静态域分配存储空间,如果必须则将解析这个类创建的对其他类的所有引用 3、初始化 如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块 仅使用.class语法来获得对类的引用不会引发初始化 Class.forName()会立即进行初始化 泛化的Class引用: 不指定类型,则理解类似于Object Class<Integer> intClass = Integer.class; //!intClass = Double.class; 错误 通配符 ---> ?:表示“任何事物” Class<? extends Number> bounded = int.class; //指定类型为所有继承与Number类的类 //还有这种:Class<? super sonClass> up = son.class.getSuperclass(); 转型语法: class Building{} class House extends Building{} public class ClassCasts{ public static void main(String[] args){ Building b = new House(); Class<House> houseType = House.class; House h = houseType.cast(b); // 等价于 h = (House)b; } } instanceof: if(x instanceof Dog){//判断对象x是否为Dog类的一个实例 ((Dog)x).bark(); } “曲线救国”:简介调用初始化块 private static void init(){ System.out.println(); } static{init();} instanceof与Class的等价性: instanceof和isInstance()生成的结果完全一样,它们保持了类型的概念,指:你是这个类么,或者你是这个类的派生类么 比较用 equals() 和 == ,它们比较的就是实际的Class对象,不会考虑继承->要么是确切的类型,要么不是 反射: java.lang.reflect类库:Method、Constructor类等 RTTI与反射的真正区别在于,对于RTTI来说,编译器在编译时打开和检查.class文件;对于反射,.class文件在编译时是不可获得的,是在运行时打开和检查.class文件 类方法提取器: 可以提取出某个类的方法以及其从基类继承来的方法 getMethods();//提取方法 getConstructors();//提取构造器 ???动态代理 337页 空对象: public interface Null{}//创建一个标记接口 class Person{ public final String first; public final String last; public final String address; public Person(String first, String last, String address){ this.first = first; this.last = last; this.address = address; } public String toString(){ return "Person: " + first + " " + last + " " + address; } //***********内部的空类用于生成空对象*********** public static class NullPerson extends Person implements Null{ private NullPerson{ super("None", "None", "None"); } public String toString[(){ return "NullPerson"; } } //空对象 public static final Person NULL = new NullPerson(); } 通过反射可以到达并调用那些非公共访问权限的方法以及访问私有的域: 例子 class WithPrivateFinalField{ private int i = 1; private final String s = "I am totally safe";//在使用反射尝试修改其值时是安全的,但实际上不会发生任何修改 private String s2 = "Am I safe?"; } public class ModifyingPrivateFields{ public static void mian(String[] args){ WithPrivateFinalField pf = new WithPrivateFinalField(); Field f = pf.getClass().getDeclaredField("i"); //获取私有的域 f.setAccessible(true); //设定此域可以被到达 System.out.println("f.getInt(pf): " + f.getInt(pf));//会将i的值打印 f.setInt(pf, 47); //修改i的值为47 ...等等 } }
泛型
泛型的主要目的之一就是用来指定 容器 要持有什么类型的对象,而且由编译器来保证类型的正确性 Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节 元组类库(tuple): 将多个返回值(可不同类型)包装成一个元组,作为return的值 public class Tuple<A, B>{ public final A a;//这里虽然是public,但是由于有final的修饰,仍可以保证其被任意读取而不能被修改 public final B b; public Tuple(A a, B b){ this.a = a; this.b = b; } } //还可以在其基础上创建更多元素的元组继承与这个基类 一个堆栈类: public class LinkedStack<T> { //Node类也有自己的类型 public static class Node<U>{ U item; Node<U> next; Node(){ item = null; next = null; } Node(U item, Node<U> next){ this.item = item; this.next = next; } boolean end() { return (item == null && next == null); } } private Node<T> top = new Node<T>(); public void push(T item) { top = new Node<T>(item, top); } public T pop() { T result = top.item; if(!top.end()) { top = top.next; } return result; } public static void main(String[] args) { LinkedStack<String> lss = new LinkedStack<String>(); for(String str: "hello world !".split(" ")) { lss.push(str); } String s; while((s = lss.pop()) != null) { System.out.println(s); } } } 泛型接口: public interface Generator<T>{} Java泛型的局限:基本类型无法作为类型参数 泛型方法:将泛型参数列表置于返回值前 public class GenericMethods{ //泛型方法,指定参数类型,啥都行 public <T> void f(T x){ System.out.println(x.getClass.getName()); } public static void main(String[] args){ GenericMethods gm = new GenericMethods(); gm.f(""); gm.f(1); gm.f(1.0); gm.f('c'); gm.f(gm); } } 泛型方法不受其类是否是泛型类的影响,单独存在 使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型 杠杆利用类型参数推断 public class New{ public static <K, V> Map<K, V> map(){ return new HashMap<K, V>(); } public static void main(String[] args){ Map<String, List<String>> sls = New.map();//类型参数推断 } } 类型推断只对赋值操作有效,其他时候并不起作用:将方法调用结果当作参数传递给另一个方法则不会进行推断 public class LimitsOfInference{ static void f(Map<String, List<String>> map){} public static void main(String[] args){ f(New.map);//!!!!出错,不会进行自动推断 } } 显示的类型说明(很少使用): 上例中的调用 f(New.<String, List<String>>map());//在点和方法名之间添加 擦除的神秘之处: public class Test{ public static void main(String[] args){ Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); } }//输出:true 在泛型代码内部,无法获得任何有关泛型参数类型的信息 Java泛型使用擦除来实现,当使用泛型时任何具体的类型信息都被擦除,唯一知道的就是在使用一个对象,List<String>和List<Integer>在运行时实际上是相同类型 指定泛型类边界: class Test<T extends MyClass>{//已知Myclass类含有f()方法 此处指定边界 private T obj; public Test(T x){ obj = x; } public void test(){ obj.f(); }//在这里才可以放心的调用f()方法 } 重点:即使擦除在方法或类内部移除了有关实际类型的信息,!!!编译器仍旧可以确保在方法或类中使用的类型的内部一致性 public class GenericHolder<T>{ private T obj; public void set(T obj){ this.obj = obj; } public T get(){ return obj; } public static void main(String[] args){ GenericHolder<String> holder = new GenericHolder<String>(); holder.set("hello world");//编译器执行传入参数检查 String s = holder.get(); //字节码中含有对get()返回的值进行转型 } } 在泛型中的所有动作都发生在边界处---对传递进来的值进行额外的 编译器检查,并插入对传递出去的值的转型 擦除的补偿----引入类标签: class ClassAsFactory<T>{ T x; public ClassAsFactory(Class<T> kind){//指定类标签 try{ x = kind.newInstance(); }catch(Exception e){ throw new RuntimeException(e); } } } 泛型数组: array = (T[])new Object[size];//泛型数组的强制类型转换 Warning:unchecked cast 补偿还是使用类型标签: Class<T> 边界:重用 extends 关键字 class Test<T extends Class & Interface1 & Interface2>{}//类在前,接口在后,类只能有一个,接口可以有很多 可以在继承的每个层次添加边界限制 interface HasColor{} class HolsItem<T>{ T item; HolsItem(){ this.item = item; } T getItem(){ return item; } } class Colored<T extends HasColor> extends HoldItem<T>{//继承了HostItem的持有对象的能力,并要求其参数与HasColor一致 Colored(T item){ super(item); } } 通配符: //条件 class Fruit{} class Apple extends Fruit{} class Jonathan extends Apple{} class Orange extends Fruit{} //例子 Fruit[] fruit = new Apple[10]; fruit[0] = new Apple();//没毛病 fruit[1] = new Jonathan();//继承于Apple,没毛病 fruit[2] = new Fruit();//ArrayStoreException fruit[3] = new Orange();//ArrayStoreException 解释:编译器认为是合法的,但是运行时的数组机制知道它处理的是Apple[],因此会在向数组中放置异构类型时抛出异常 //编译时错误,不相容类型 List<Fruit> flist = new ArrayList<Apple>();//Apple的List在类型上不等价于Fruit的List 这根本不是向上转型 List<? extends Fruit> flist = new ArrayList<Apple>();//一旦执行这种类型的向上转型,将丢掉向其中传递任何对象(set)的能力,甚至是Object 逆变: 超类型通配符,可以声明通配符是由某个特定类的任何基类来界定的 <? super MyClass> <? super T> *通配符: <?> 它是在声明:我是想用Java的泛型来编写这段代码,我这里并不是要原生类型,但是在当前这种情况下,泛型参数可以持有任何类型 <?>表示一种特定的类型,但是我现在还不知道它是啥,原生类型则是任何Object类型 一个原则: <? extends MyClass> 指明的对象至少是MyClass,可以使用get返回值并转型为MyClass(来自Myclass的两个不同的东西不具有兼容性,没法存) <? super MyClass> 指明的对象是MyClass的某个基类,可以使用set将MyClass存储(根据多态一定可以操作MyClass,取出来的类型有很多种,所以就没法取) 泛型使用时常见问题: 任何基本类型都不能作为类型参数:new ArrayList<int>();//int是不被允许的 使用包装类 转型和警告:使用带有泛型类型参数的转型或 instanceof 不会有任何效果(由于存在擦除) 泛型方法的重载: public class UseList<W, T>{ void f(List<T> v){}//由于擦除会导致产生相同的类型签名 void f(List<W> v)()//出现这中情况就使用具有明显区别的方法名 } 基类劫持接口: public class ComparableClass implements Comparable<ComparableClass>{ public int compareTo(ComparableClass arg){ return 0; } } class ExtendedClass extends ComparableClass implements Comparable<ExtendedClass>{}//错误,基类实现的是ComparableClass对象的比较,这里无法指定新的对象 自限定类型: 古怪的循环泛型(DRG):类相当古怪的出现在它自己的基类中 理解:我正在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数 class GenericType<T>{} public class MyType extends GenericType<Mytype>{} 自限定: class SelfBounded<T extends SelfBounded<T>>{} class A extends SelfBounded<A>{}//基类中指定的类型要是继承于基类的类型 class D{} class E extends SelfBounded<D>{}//这样是不可以的 如果不使用自限定,将重载参数类型,如果使用了自限定,只能获得 某个方法 的一个版本,它将接受确切的参数类型(使用自限定类型就确定): 自限定类型的价值在于它可以产生 协变参数类型 --- 方法参数类型会随子类而变化 interface SelfBoundSetter<T extends SelfBoundSetter<T>>{ void set(T arg); } interface Setter extends SelfBoundSetter<Setter>{} public class SelfBoundingAndCovariantArguments{ void testA(Setter s1, Setter s2, SelfBoundSetter sbs){ s1.set(s2); //s1.set(sbs); //Error 自限定类型 } } 泛型异常---目前不会用吧 410页 Java混型|--与接口混合 |--使用装饰器模式 |--与动态代理混合 泛型的广泛应用:持有器(持有对象) 潜在类型机制:Java缺乏潜在类型机制 对其的补偿: 反射:通过Class对象尝试获取某对象的方法 问题:将所有的类型检查都转移到了运行时,因此在许多情况下是我们所不希望的 ???函数对象:就是某种程度上行为像函数的对象---与普通方法不同,它们可以传递出去,并且还可以拥有在多个调用之间持久化的状态 论点:使用泛型类型机制的最吸引人的地方,就是在使用容器类的地方,这些类包括各种List、各种Set、各种Map等等 当将一个对象放置到容器中时,这个对象就会被向上转型为Object,因此将会丢失类型信息 当将一个对象从容器中取回,用它去执行某些操作时,必须将其向下转型回正确的类型
数组
数组与其他种类的容器之间的区别有三方面:效率、类型、保存基本类型的能力 数组是一种效率最高的存储和随机访问对象引用序列的方式 使用数组和容器时,如果越界到会得到一个 表示程序员错误的 RuntimeException Java返回数组(引用):无需考虑像C或C++那样内存回收或内存泄漏问题(数据存在堆内的原因?) 无需担心要为数组负责,只要需要它就会一直存在,使用完后垃圾回收器会清理掉它 Arrays.deepToString()方法:将多维数组转换为多个String Arrays.deepToString(new int[][]{{1, 2, 3}, {4, 5, 6}});//输出[[1, 2, 3], [4, 5, 6]] 数组中构成矩阵的每个向量都可以具有任意长度---粗糙数组: int[][] is = new int[][]{ {1, 2, 3}, {1, 2}, {5, 6, 7, 8} }; 数组必须知道它们所持有的确切类型,以强制保证类型安全 数组与泛型不能很好的结合,不能实例化具有参数化类型的数组: !!!编译器不允许实例化泛型数组 T[] t = new T[10];是不允许的 Peel<Banana>[] pells = new Peel<Banana>[10];//错误 可以参数化数组本身的类型: class Class<T>{ public T[] f(T[] arg){ return arg; //doSomething }// } 还可以创建数组引用: List<String>[] ls; List[] la = new List[10]; ls = (List<String>[])la;//"Unchecked" warning //数组是协变类型 List<String>[] 也是一个 Object[] Object[] objects = ls; //此时就可以存入其他类型 objects[1] = new List<Integer>(); 数组统一初始化:Arrays.fill();方法 int[] is = new int[6]; Arrays.fill(is, 2); Array.fill(is, 2, 5, 3);//index为2、3、4的位置填充替换为3 System.out.println(Arrays.toString(is));//[2, 2, 3, 3, 3, 2] 数据生成器:Generator Arrays使用功能:一套用于数组的static方法 equals(); ----比较两个数据是否相等(deepEquals()用于多维数组) 基于内容的比较 Object.equals() fill(); ----填充数组 sort(); ----对数组排序 默认从小到大 需要排序的对象必须实现Comparable接口(重写compareTo(MyType another)方法)sort需要将参数的类型转变为Comparable Arrays.sort(array, Collections.reverseOrder());//反序排列 Collections.reverseOrder()产生了一个Comparator 编写自己的Comparator class MyTypeComparator implements Comparator<Mytype>{ public int compare(Mytype o1, MyType o2){ return (o1.i < o2.i ? -1 : (o1.i == o2.i ? 0 : 1)); } } Arrays.sort(array, new MytypeComparator()); 针对基本类型采用“快速排序” 针对对象采用“稳定归并排序” binarySearch(); ----在已经排序的数组中查找元素 也可以使用Comparator: Arrays.binarySearch(array, array[3], String.CASE_INSENSITIVE_ORDER);//字符串忽略大小写 toString(); ----数组转换为String hashCode(); ----产生数组的案列码 asList(); ----接受任意的序列或数组为参数,并将其转变为List容器 数组复制: Java标准类库提供有static方法 System.arraycopy() 用法:System.arraycopy(源数组, 源数组偏移量, 目标数组, 目标数组偏移量, 复制元素的个数) 基本数组以及对象数组都可以复制,对象数组的复制只是复制引用(浅拷贝) 注:System.arraycopy 不会执行自动包装和自动拆包,两个数组必须具有相同的确切类型 总结:Java编程中,优先选用容器而不是数组,只有在已证明性能成为问题(切换到数组会有大的提升)时,才应该将程序重构为使用数组
容器深入研究
填充容器: Collections.nCopies()创建传递给构造器的List: List<String> list = new ArrayList<String>(Collections.nCopies(4, new String("hello")));//之间填充4个相同的对象(引用) Collections.fill()只对List有用: Collections.fill(list, new String("world"));//只能替换已经在List中的元素,不能添加新的元素 直接输出一个对象的引用得到:类型@该对象的散列码的无符号十六进制 所有的Collection子类型都有一个接收另一个Collection对象的构造器,用所接受的Collection对象中的元素来填充新的容器 一种Generator解决方案: interface Generator<T>{ T next(); } public class CollectionData<T> extends ArrayList<T>{ public CollectionData(Generator<T> gen, int num){ for(int i = 0; i < num; ++i){ add(gen.next()); } } //通用便捷方法 传递进来任意实现生成器接口的类型,就可以得到持有此类型的CollectionData public static <T> CollectionData<T> list(Generator<T> gen, int num){ return new CollectionData<T>(gen, num); } } //一个实现生成器的类 class MyClass implements Cenerator<Integer>{ Random rand = new Random(49); public Integer next(){ return rand.nextInt(100); } } //在main中测试 Set<Integer> set = new LinkedHashSet<Integer>(new CollectionData<Integer>(new MyClass(), 3));//使用CollectionData进行填充 set.addAll(CollectionData.list(new MyClass(), 3));//使用便捷方式 static list()方法 再次复习Iterable接口的使用: class MyType implements Iterable<Integer>{ private int size = 9; private int number = 1; @Override //重点 public Iterator<Integer> iterator(){ return new Iterator<Integer>(){ public Integer next(){ return number++; } public boolean hasNext(){ return number < size; } public void remove(){ throw new UnsupportedOperationException(); } } } } 使用Abstract类:创建定制的Collection和Map实现自己需求(目前可能用不到) Collection的功能方法: boolean add(T);//添加元素 ^ boolean addAll(Collection<? extends T>);//添加所有元素 ^ void clear();//清空容器 ^ boolean contains(T);//是否持有参数 Boolean containsAll(Collection<?>);//是否持有所有元素 Iterator<T> iterator();//返回一个Iterator<T>,用于遍历容器 Boolean remove(Object);//移除此元素的一个实例 ^ boolean removeAll(Collection<?>);//移除参数中所有元素 ^ Bollean retainAll(Collection<?>);//只保留参数中的元素 ^ int size();//容器中元素数目 Object[] toArray();//返回持有所有元素的数组 <T> T[] toArray(T[] a);//返回与a相同类型的数组 用法举例:String[] strs = myList.toArray(new String[0]); 注意:这些都是Collection的方法,对于List和Set适用,Map不是继承自Collection无法使用 而且对于List和Set还会有特定的一些方法实现更多的功能 执行各种不同的添加和移除的方法在Collection接口中都是可选操作,这意味着实现类并不需要为这些方法提供功能定义 ^:标注为可选操作 常用的容器类内这些可选操作都得到了具体实现,都支持所有操作 可选操作在运行时可能会抛出 UnsupportedOperationException 未获支持的操作:add() addAll() retainAll() 等 来源于背后由固定尺寸的数据结构支持的容器 ---> Arrays.asList() 基于一个固定大小的数组,仅支持那些不会改变底层数组大小的操作,比如修改 set() 是可以的 通过 Collections.unmodifiableList() 构建的不可修改的容器 由于指定是不可修改,所以都不可以 使用迭代器也可以修改元素: public void iterTest(List<String> aList){ ListIterator<String> it = aList.listIterator(); it.add("47"); it.previous();//在前面添加了一个后把指针移到最前面 it.remove(); it.next();//把当前的元素删除,指针移动到下一个 it.set("47");//修改当前指针位置的元素 } 所有在Set中存储的类都必须具有 boolean equals() 方法 HashSet、LinkedHashSet中存储的类都必须具有 hashCode() 方法(这里看到的是返回 int类型) TreeSet中存储的类都必须实现 Comparable接口 并实现 compareTo() 方法: 注意:不要使用简明的 return (i - i2); 的形式 例如i是很大的正整数 i2是很大的负整数? i-i2可能会溢出返回负值 总结:所有存入Set类必须具有 equals() 方法 所有存入HashSet的类必须还要具有 hashCode() 方法 所有存入任何种类的排序容器中必须实现 Comparable 接口 SortedSet: 实现此接口的类可以调用Comparator comparator() 方法返回当前Set使用的Comparator (返回null表示以自然方式排序) 还有: Object first();//返回容器中第一个元素 Object last();//返回容器中的最后一个元素 SortedSet subSet(fromElement, toElement);//生成Set的子集 左闭右开 SortedSet headSet(toElement);//生成子集,元素小于参数 SortedSet tailSet(fromElement);//生成子集,元素不小于参数 队列: 两个实现:LinkedList PriorityQueue 差异在于排序行为而不是性能 优先级队列 双向队列(没有具体声明 靠LinkedList实现) Map: HashMap//默认Map,速度最快 TreeMap LinkedHashMap WeakHashMap ConcurrentHashMap IdentityHashMap hashCode()方法是Object中的方法,所有Java对象都能产生散列码 默认使用对象的地址计算散列码 map.keySet();//返回Map的键的Set map.values();//返回Mao的值的Collection SortedMap(TreeMap是其唯一实现)的使用跟SortedSet很像,方法什么的也都差不多 为Map创建自己的key: 注意的两个方法 hashCode() equals() //两者都在Object类中都有 不覆盖会使用默认的形式 equals()方法必须满足的5个条件: 1、自反性 对于任意的x x.equals(x)一定返回true 2、对称性 对于任意的x和y 如果y.equals(x)返回true,则x.equals(y)也一定返回true 3、传递性 对于任意x、y、z x.equals(y)返回true y.equals(z)也返回true 则x.equals(z)也返回true 4、一致性 对于任意的x和y,如果对象中用于等价比较的信息没有改变,那么无论调用 x.equals(y) 多少次返回的结果应该保持一致 5、对于任何 不是null的x,x.equals(null)一定返回false 线性查询是最慢的查询方式 为速度而散列: 创建一个固定大小的 LinkeedList数组,每一个数组位置称为桶位(bucket) 基于Key计算哈希值作为数组的index,如果put时index位置为null,则创建一个LinkedList 并将引用存储在相应位置的桶位上 为了散列分布均匀,桶的数量通常使用质数 广泛测试结果:使用2的整数次方的散列表速度更快(掩码代替耗时的除法) 散列码不必是独一无二的,散列码关注的是生成速度 String的特点:如果程序中有多个String对象,都包含相同的字符串序列,那么这些String对象都映射到同一块内存区域 String的hashCode()是基于String的内容生成的(不同的引用指向同意内存地址) 自己覆盖hashCode()生成根据需要的哈希值 TreeSet 和 TreeMap都实现了元素的排序: 树的行为是总是保证有序,并且不必进行特殊的排序。一旦填充了一个TreeMap,就可以调用keySet()方法获取键的Set视图,然后调用toArray()方法来 产生由这些键构成的数组,之后可以使用Arrays.binarySearch()方法在排序的数组中快速查找对象 默认使用情况:ArrayList HashSet HashMap HashMap的性能因子: *容量:桶个数 *初始容量:表在创建时拥有的同位数,HashMap和HashSet都具有指定初始容器容量的构造器 *尺寸:表中当前存储的项数 *负载因子:尺寸/容量(HashMap默认负载因子 0.75 ) 大量的容器实用方法: java.util.Collections 类内部的静态方法(static) 512页 list的排序使用Comparator binarySearch()的时候也要使用相同的Comparator 不可修改容器: Collections.unmodifiable* (*: Collection Set List Map SortedSet SortedMap) 使用时可以事先构造一个private的正常容器,填充好数据(设定为不可修改的必须条件)后作为参数传递给此方法 然后返回不可修改容器的引用 Collection和Map的同步控制: Collections类有办法能够自动同步整个容器 Collections.synchronized*() *同样代表各种容器 //直接将新生成的容器传递给适当的“同步”方法 保证没有任何机会暴露出不同步的版本 Collection<String> c = Collections.synchronizedCollection(new ArrayList<String>()); 快速报错: Java容器有一种保护机制,能够放置多个进程同时修改同一个容器的内容。 如果在迭代遍历某个容器的过程中,另一个进程介入其中,并且插入、删除、或修改此容器内某个对象,那么就会出现问题 快速报错机制(fail-fast)探查容器上的任何处理自己的进程所进行的操作以外的所有变换,一旦发现其他进程修改容器,就会立刻抛出ConcurrentModificationException异常 对象是可获得的:指此对象可在程序中的某处找到 此对象的引用在栈内找到(或是存在中间链接 a->b->c) 对象是可获得的则垃圾回收器就不能释放它,因为它仍然为程序所用 如果是不可获得的则垃圾回收器将其回收是安全的 Reference类再研究(WeakHashMap)
Java I/O系统
File类: 帮助我们处理文件目录问题 目录列表器 list() 方法 此方法接收空参数 以及一个实现了FilenameFilter接口的类的实例 public interface FilenameFilter{ boolean accept(File dir, String name);//list() 会回调accept方法 } 此处复习:某方法内部创建匿名内容类需要传递进参数,必须是final(编译器强制要求 为了保持应用的的正确性,不被修改) 流:它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象 “流”屏蔽了实际的I/O设备中的处理细节 InputStream: 作用是用来表示那些从不同数据来源产生输入的类 ByteArrayInputStream//允许将内存的缓冲区当作InputStream 构造器参数为 缓冲区, 字节将从中取出 StringBufferInputStream//将String转换成InputStream FileInputStream//文件 PipedInputStream//管道 SequenceInputStream//将多个InputStream对象转换成单一InputStream FilterInputStream//抽象类,作为“装饰器”的接口 为其他的InputStream类提供有用的功能 OutputStream: 决定输出所要去往的目标 ByteArrayOutputStream FileOutputStream PipedOutputStream FilterOutputStream 我们几乎每次都是对输入进行缓冲---不管我们正在连接的是什么I/O设备,I/O类库把无缓冲输入作为特殊情况 FilterInputStream常用类型: DataInputStream//可以按照可移植方式从流读取基本数据类型(int, char, long等) BufferedInputStream//防止每次读取时都得进行写操作 DataOutputStream PrintStream BufferedOutputStream InputStream和OutputStream面向字符形式的I/O Reader和Writer提供兼容Unicode和面向字符的I/O功能 InputStreamReader 将InputStream转换为Reader OutputStreamWriter 将OutputStream转换为Writer 总结一点:从面向字节流的InputStream / OutputStream 转换为面向字符的Reader / Writer 使用相应的适配器 *Reader *Writer 自我独立的类:RandomAccessFile//只适用于文件 构造器需要额外参数: "r"//随机读 "rw"//随机读写 getFilePointer()//查找当前所处的文件位置 seek()//在文件内移至新的位置 length()//判断文件最大尺度 I/O流的典型使用方法: 缓冲输入文件: 打开文件用于字符输入 FileInputReader 为了提高速度使用缓冲 将所产生的引用传给一个BufferedReader构造器 BufferedReader也提供 readLine() 方法 到文件末尾返回null BufferedReader in = new BufferedReader(new FileReader(filename)); //BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(filename))); String s; while(s = in.readLine() != null){ System.out.print(s + "\n");//readLine() 会删除掉每一行后面的换行符 } in.close();//不要忘了用完要关闭文件 从内存输入//不常用 格式化的内存输入//不常用 小窍门:跟内存/缓存有关的 都是Buffer 基本的文件输出: PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(filename)));//PrintWriter提供格式化输出 //BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename))); 写入文件时:PrintWriter提供print相关方法 BufferedWriter提供write方法(还有一个newLine) 特别推荐的快捷方式:直接PrintWriter out = new PrintWriter(filename);//仍旧会使用缓存 存储和恢复数据:格式化读与写 DataInputStream DataOutputStream 含有针对不同内容的读写操作 如 readDouble() 读写随机访问文件: RandomAccessFile();//构造器接收文件名以及一个打开方式 "r" "rw" 它不支持装饰,所以不能将其与InputStream以及OutputStream子类的任何部分组合起来 文件读写的实用工具: 确保处理文件后关闭文件: try{ PrintWriter out = new PrintWriter(new FileReader(filename)); try{ for(String str: strArrays){ out.println(str); } }finally{ //这里保证了处理完文件后及时关闭文件 out.close(); } }catch(IOException e){ throw new RuntimeException(e); } 读取二进制文件: 使用字节流 BufferedInputStream bf = new BufferedInputStream(new FileInputStream(bFile)); try{ byte[] data = new byte[bf.available()];//bf.available()返回文件占有的byte大小 bf.read(data); return data; }finally{ //再次复习:上面return了 也会执行finally的代码 bf.close(); } 标准I/O: System.out//事先被包装成了printStream System.err//事先被包装成了printStream System.in//没有被包装过的未经加工的InputStream 使用它之前必须对其进行包装 //包装如:BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 标准I/O重定向://不太清楚怎么用(没有进行实验) 静态方法: System.setIn(); System.setOut(); System.setErr(); 通道与缓冲器:通道(Channel)->包含煤层(数据)的矿藏 缓冲器->用于矿藏与外界进行资源交互的卡车 唯一直接与通道交互的缓冲器是 ByteBuffer ByteBuffer是将数据移进移出通道的唯一方式 视图缓冲器:针对基本类型 比如CharBuffer 内存映射文件:(MappedFile) 允许我们创建和修改那些因为太大而不能放入内存的文件 FileChannel fc = new RandomAccessFile("temp.tmp", "rw").getChannel(); IntBuffer ib = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size()).asIntBuffer(); for(int i; i < 10; ++i){ ib.put(i); } fc.close(); 文件加锁: public class FileLocking{ public static void main(String[] args) throws Exception{ FileOutputStream fos = new FileOutputStream("file.txt"); FileLock fl = fos.getChannel().tryLock(); if(fl != null){ System.out.println("Locked File"); TimeUnit.MILLISECONDS.sleep(100); fl.release(); //释放锁 System.out.println("Released File"); } fos.close(); } } 通过对FileChannel调用tryLock()或者lock() 就可以获得整个文件的FileLock 注:SockedChannel、DatagramChannel和ServerSocketChannel不需要加锁,因为他们是从单进程实体继承而来的 tryLock()是非阻塞的,它设法获取锁,但是如果不能获得(当其他进程已经持有相同的所并且不共享时),它将直接从方法调用中返回 lock()是阻塞的,它要阻塞进程直至所可以获得,或者调用lock()的线程中断,或者调用lock()的通道关闭 可以对文件的一部分上锁: tryLock(long position, long size, boolean shared); lock(long position, long size, boolean shared);//加锁区域为 position ~ position+szie shared指定是否为共享锁 对 独占锁 和 共享锁 的支持必须由底层的操作系统提供,如果操作系统不支持共享锁并为每个请求都创建一个锁,那么它就会使用独占锁 通过 FileLock.isShared() 进行查询锁的类型 压缩//应该不会用到 Zip GZIP JAR文件: 一种压缩的文件格式 JAR文件是跨平台的,所以不必担心跨平台的问题 声音和图像文件可以像类文件一样被包含在其中 最实用: jar cf myJarFile.jar *.class //Jar文件包含所有的class文件 571页 对象序列化与反序列化://目的就是将含有信息的对象存入磁盘 并在需要的时候将其恢复 序列化接口 Serializable 实现了此接口的类可以支持序列化 序列化: public class Alien implements Serializable{} public class FreezeAlien{ public static void main(String[] args) throws Exception{ ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("X.file")); Alien quellek = new Alien(); out.writeObject(quellek); } } 反序列化: public class ThawAlien{ public static void main(String[] args) throws Exception{ ObjectInputStream in = new ObjectInputStream( new FileInputStream(new File("..", "X.file"))); Object mystery = in.readObject(); System.out.println(mystery.getClass); } } 注:打开文件和读取mystery对象中的内容都需要Alien的Class对象;如果Java虚拟机找不到Alien.class就会得到ClassNotFoundException 必须保证Java虚拟机能够找到相关的.class文件 序列化的控制: Externalizable 继承了Serializable 并添加了两个方法:writeExternal() readExternal()//这两个方法在序列化与反序列化的过程中被自动调用来执行一些特殊操作 Externalizable对象在反序列化时 它的构造器必须是public//否则恢复时造成异常 注:Serializable对象的恢复完全以它的二进制位为基础来构造,不调用构造器 Externalizable对象会调用默认构造器 Externalizable类内的域要在writeExternal() 以及 readExternal() 中进行显示序列化与反序列化 两种对象序的列化的区别:Serializable 全部会自动进行 Externalizable没有任何东西可以自动序列化 transient(瞬时)关键字: 如果使用的是Serializable构建类 但是希望其中的某一个子类(域)不被序列化存储(如密码)则需要在此域前加 transient关键字 Externalizable的替代方法: 实现Serializable接口的类中定义 writeObject() 以及 readObject()方法 方法必须具有准确的方法签名: private void writeObject(ObjectOutputStream stream) throws IOException; private void readObject(ObjectOutputStream stream) throws IOException, ClassNotFoundException; 注:这两个方法是 ObjectOutputStream 以及 ObjectInputStream 的writeObject() 和 readObject() 方法来调用的//private之所以可以被调用使用的是 反射机制 可以在自己的这两个方法中调用defaultWriteObject() 和 defaultReadObject() 来选择执行默认的write和read 使用“持久性”: 只要将任何对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,并没有任何意外重复复制出的对象 如果我们想保存系统状态,最安全的做法就是将其作为“原子”操作进行序列化: 将构成系统状态的所有对象都置于单一容器内,并在一个操作中将该容器直接写出 然后同样只需一次方法调用即可将其恢复 对于类内static值不会被自动序列化,想要序列化static值,必须自己手动去实现 XML: import nu.xom.*; 可以将对象保存为规范格式的.xml文件 具体参考 586页 读取xml文件并解析对象//反序列化 public class People extends ArrayList<Persion>{ public People(String filename) throws Exception{//传入文件名的构造器 Document doc = new Builder().builder(filename);//打开文件 基于xom类库 Elements elements = doc.getRootElement().getChildElements();//得到一个Elements列表 拥有get() 和 size() 方法 for(int i = 0; i < elements.size(); ++i){//size确定大小 add(new Persion(elements.get(i))); //get获取元素 } } public static void main(String[] args) throws Exception{ People p = new People("People.xml"); System.out.println(p); } } Preferences API: 588页 他只能用于小的、受限的数据集合---我们只能存储基本类型和字符串(每个字符串的存储长度不能超过8K) Preferences是一个键-值集合(类似映射),存储在一个节点层次结构中
枚举类型
enum基本特性: 创建enum时,编译器会自动生成一个相关的类,这个类继承于java.lang.Enum 方法values() 返回enum实例的数组 可以用于遍历 ordonal() 返回int值 对应于每一个实例的声明时的次序(从0开始) 可以使用 == 比较实例 编译器会自动提供equals() 和 hashCode() 方法 Enum类实现了Comparable接口 所以enum实例具有compareTo() 方法 enum实例上可以调用 getDeclaringClass() 查看它所属的类 Enum类的静态方法valueOf() 根据给定的名字返回相应的enum实例: enum E { ONE, TWO, THREE} Enum.valueOf(E.class, "ONE");//得到ONE对应的enum实例 将 import static 用于enum: 可以将enum实例的标识符带入当前的命名空间,无需在用enum类型来修饰enum实例: class Test{ E em; Test(E em){ this.em = em; } } 不使用静态导入: new Test(E.ONE) 使用: new Test(ONE); 注意:在定义enum的同一个文件中 或者 在默认包中定义enum 无法使用此技巧 enum不能被继承,除了这一点几乎可以将其看成为类,可以在其中添加方法,甚至它可以有main()方法 在enum中定义自己的方法: 注意几点:创建enum实例的时候可以采用括号后传入字符串参数作为描述//字符串作为其构造器的参数 实例后面定义方法时,实例定义结束要加分号 构造器默认是private 一旦enum的定义结束,编译器就不允许我们再使用其构造器来创建任何实例 也可以像正常的类一样,覆盖已有的方法 switch语句中可以使用enum:正常情况下,需要使用enum类型来修饰一个enum实例,但是在case语句中却不需要 values()的神秘之处: enum E { ONE, TWO } 反编译:java的代码可以调用系统命令 OSExecute.command("javap E"); 得到: final class E extends java.lang.Enum{ public static final E ONE; public static final E TWO; public static final E[] values(); public static E valueOF(java.lang.String); static {};//静态初始化子句 } 解释:编译器将 E 标记为final类,所以无法被继承 values()是由编译器添加的static方法 编译器还添加了valueOf()方法//Enum类的valueOf()需要两个参数 添加的这个需要一个 values()方法是由编译器插入到enum定义中的static方法,向上转型为Enum后values()方法无法访问,可以使用Class的getEnumConstants()方法得到所有enum实例 enum继承于Enum所以无法再继承其他类,但是可以implements接口 使用接口组织枚举: 在一个接口的内部,创建实现该接口的枚举,以此将元素进行分类 public interface Food{ enum Appetizer implements Food{ SALAD, SOUP, SPRING_ROLLS; } enum MainCourse implements Food{ LASAGNE, BURRITO, PAD_THAI; } enum Dessert implements Food{ TIRAMISU, GELATO, BLACK_FOREST_CAKE; } }//所有的枚举实例都是Food类 它们又被进行了分类 EnumSet: EnumSet中的元素必须来自一个enum 使用一个long(64位)值作为比特向量 一个enum实例只需一位bit表示其是否存在 EnumSet可以应用于最多不超过64个元素的enum 当enum超过64个,EnumSet会在必要的时候增加一个long 向其中添加enum实例的顺序并不重要,因为其输出的次序决定于enum实例定义时的次序 各种方法查阅资料使用 EnumMap: EnumMap中的key必须来自一个enum 构建Map: EnumMap<MyEnum, MyClass> em = new EnumMap<MyEnum, MyClass>(MyEnum.class); enum的每一个实例作为一个键,总是存在 如果没有为这个键调用put()方法来存入相应的值的话,其对应的值就是null 常量相关方法: public enum ConstantSpecificMethod{ ONE{ void action(){ System.out.println("ONE"); } } TWO{ void action(){ System.out.println("TWO"); } }; abstract void action();//需要定义抽象方法 } 调用:ONE.action(); enum实例不可以作为一个类型来使用: //void f1(ConstantSpecificMethod.ONE instance){} ONE是一个静态实例 不能作为类型 除了实现abstract方法外 还可以覆盖常量相关方法 public enum ConstantSpecificMethod{ ONE, TWO{ void action(){ System.out.println("TWO"); } }; void action(){ ; } } Java只支持单路分发: 如果要执行的操作包含了不止一个类型未知的对象时,那么Java的动态绑定机制只能处理其中一个的类型 所以我们必须自己来判定其他的类型,从而实现自己的动态绑定行为 使用enum分发:615页代码 不太理解构造器的代码部分 private Outcome的 三个实例如何初始化(初始化为啥呢!!??) 还有使用 常量相关方法 以及 EnumMap 和 数组:核心思路就是构建各种不同类型的表而已 “表驱动式编程”
注解
注解为我们在代码中添加信息提供了一种格式化的方法,是我们在稍后某个时刻非常方便地使用这些数据 Java SE5内置三种注解,定义在java.lang中: @Override 表示当前的方法定义将覆盖超类中的方法 @Deprecated(弃用) 如果程序员使用了注解为Deprecated的元素,编译器会发出警告 @SupressWarnings 关闭不当的编译器警告 每当创建描述符性质的类或接口时,一旦其中包含了重复性的工作,就可以考虑使用注解来简化与自动化该过程 定义注解: 注解定义后,其中可以包含一些元素来表示某些值 分析处理注解时,程序或工具可以利用这些值 没有元素的注解是 标记注解 例: @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Test{} 含有元素的注解定义: @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface UseCase{ public int id(); //元素定义类似于方法 public String description() default "no description";//设置默认值 如果添加注解时没有指定 则使用默认值 } 用注解: @UseCase(id = 47, description = "whatever you want to write") public void oneMethod(){} 四种元注解 @Target 用来定义我们自己定义的注解将应用于什么地方(例如是一个方法或者一个域)(ElementType.) @Retention 用来定义该注解在哪一个级别可用 源码:OURCE 类:CLASS 运行时:RUNTIME(RetentionPolicy.) @Documented 将注解包含在Javadoc中 @Inherited 允许子类继承父类中的注解 注解元素可用的类型: 基本类型 String Class enum Annotation 以上类型的数组 元素默认值限制: 元素必须要么具有默认值,要么在使用注解时提供元素的值 对于非基本类型的元素,默认值定义不能为null 所以在定义时通常以负数或空字符串来表示某个元素不存在 元素命名为value,使用注解时value是唯一需要赋值的元素则可以直接在括号里写如值 而无需使用键=值的形式 注意:注解不支持继承 基于注解的单元测试: 单元测试 是对类中的每个方法提供一个或多个测试的一种实践,其目的是为了有规律地测试一个类的各个部分是否具备正确的行为 基于注解的测试框架:@Unit 用的最多的@Test 用@Test标记测试方法 被标记的测试方法不带参数 返回boolean 或 void 例子: @Test boolean assertAndReturn(){ assert 1 == 2: "What a surprise!"; return methodOne().equals("") } 注:使用@Unit进行测试必须定义在某个包中(即必须包括package声明 例如java mywork.mypackage.xlc MyTest) 对于每一个单元测试而言,@Unit都会用默认的构造器,为该测试所属的类创建出一个新的实例 并在此新创建的对象上运行测试,然后丢弃该对象,以免对其他测试产生副作用 如果没有默认浏览器,或者新对象需要复杂的构造过程,可以创建一个 static方法专门负责构造对象 使用 @TestObjectCreate注解标记此方法(必须是static的方法) 测试单元中添加额外的域 使用 @TestProperty注解(还可以用来标记那些只在测试中使用的方法,而他们本身又不是测试方法) 进行清理工作使用 @TestObjectCleanup 标记static方法 通过继承实现测试类 来完成@Unit对泛型的使用: 唯一缺点是 继承使我们失去了访问被测试的类中的private方法的能力 解决办法:设定为protected 或 使用非private的@TestProperty方法调用private方法 总结:Java SE5仅提供了很少的内置的注解 如果我们在别处找不到可用的类库,就只能自己创建新的注解以及相应的处理器 借助apt工具,我们可以同时编译新产生的源文件,以及简化构建过程 mirror API只能提供基本功能--帮助找到Java类定义中的元素 Javassist能够用来操作字节码
并发
用并发解决的问题大体上可以分为 速度 和 设计可管理性 阻塞: 如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或线程 阻塞 了 进程: 是运行在它自己的地址空间内的自包容的程序 多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程(程序) 进程会被互相隔离开,因此它们不会彼此干涉 与在多任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的 单一进程 中创建任务 Java的线程机制是 抢占式 的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每一个线程都提供时间片 使得每个线程都会分配到数量合理的时间去驱动它的任务 定义任务: 一种描述任务的方式:由Runnable接口来提供 要想定义任务,只需要实现Runnable接口并编写run()方法,使得该任务可以执行你的命令 public class RunClass implement Runnable{ ... public void run(){ while(...){ ... Thread.yield(); } } } 注:任务的run方法通常总会有某种形式的循环,使得任务一直运行下去直到不再需要 所以要设定跳出循环的条件(有一种选择是直接从run()返回) 通常run被写成无限循环的形式 静态方法Thread.yield()是对线程调度器的一种建议:我已经执行完最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机 Thread类: 将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器: public class BasicThreads{ public static void main(String[] args){ Thread t = new Thread(new RunClass());//Thread构造器只需要一个Runnable对象 t.start(); System.out.println("Waiting for RunClass"); } } 注:调用Thread对象的start()方法为该线程执行必要的初始化操作,然后调用Runnable的run()方法 程序会同时运行两个方法,main()和RunClass.run() 使用Executor: 用单个Executor 来创建和管理系统中所有的任务 例子: public class Executor{ ExecutorService exec = Executors.newCachedThreadPool(); for(int i = 0; i < 5; ++i){ exec.execute(new RunClass); } exec.shutdown();//防止新任务被被提交给这个Executor } newCachedThreadPool是首选 还有: FixedThreadPool(指定线程数) SingleThreadPool //相当于FixedThreadPool(1) 会序列化所有提交给它的任务 逐个完成 Runnable是执行工作的独立任务,但是不会返回任何值 如果希望任务在完成时能够返回一个值,则可以实现 Callable接口 Callable是一种具有参数类型的泛型,它的类型参数表示的是从方法call()中返回的值,必须使用ExecutorService.submit()方法调用它 例子: class TaskWithResult implements Callable<String>{ private int id; public TaskWithResult(int id){ this.id = id; } public String call(){ return "result of TaskWithResult " + id; } } public class CallableDemo{ public static void main(String[] args){ ExecutorService exec = Executors.newCachedThreadPool(); ArrayList<Future<String>> results = new ArrayList<Future<String>>(); for(int i = 0; i < 10; ++i){ results.add(exec.submit(new TaskWithResult(i))); } for(Future<String> fs: results){ try{ System.out.println(fs.get()); }catch(InterruptedException e){ System.out.println(e); return; }catch(ExecutionException e){ System.out.println(e); }finally{ exec.shutdown(); } } } } 注:submit()方法会产生Future对象,此处将指定类型的返回值作为参数传递给submit()方法 //可以使用 isDone()方法来查询Future是否已经完成 //直接使用get()方法在Future没有完成时会形成阻塞,直到结果准备就绪 休眠: sleep() 老式的sleep: Thread.sleep();//单位是毫秒 新式的sleep: TimeUnit.MILLISECONDS.sleep();//可以指定sleep()延迟的时间单元,具有更好的可阅读性 对sleep的调用会抛出InterruptedException 异常 异常不能跨线程传播回主线程,所以必须在本地处理所有在任务内部产生的异常 优先级: 线程的优先级将该线程的重要性传递给调度器 调度器倾向于让优先级最高的线程先执行 这并不意味着优先级较低的线程得不到执行(优先级不会导致死锁) 只是优先级较低的线程执行的频率较低 可以使用getPriority()来读取现有线程的优先级 setPriority()来修改线程的优先级 public void run(){ Thread.currentThread().setPriority(priority);//currentThread:当前线程 } 常用优先级: MAX_PRIORITY//最大 NORM_PRIORITY//标准 MIN_PRIORITY//最小 让步: Thread.yield()方法 如果知道已经完成了在run()方法的循环的一次迭代过程中所需的工作,就可以给线程调度机制一个暗示: 你的工作已经做得差不多了,可以让别的线程使用CPU了 注意:对于任何重要的控制或在调整应用时,都不能依赖于yield() 后台线程: 是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分 使用Thread的setDaemon() 方法将线程设置为后台线程: Thread daemon = new Thread(new SimpleDaemon()); daemon.setDaemon(true);//设置为后台线程 daemon.start(); 被设置为后台线程的线程,其派生出来的其他子线程,都是后台线程(不需要显示地设置为后台线程) 后台线程中 不会执行finally子句的情况下就会终止其run()方法 直接继承与Thread类: public class SimpleThread extends Thread{ private int contDown = 5; private static int threadCount = 0; public SimpleThread(){ super(Integer.toString(++threadCount)); start(); } public String toString(){ return "#" + getName() + "(" + countDown + "),"; } public void run(){ while(true){ System.out.println(this); if(--countDown ==0){ return; } } } public static void main(String[] agrs){ for(int i = 0; i < 5; i++){ new SimpleThread(); } } } * 可以定义内部类来将线程代码隐藏在类中 * 还可以在类的内部定义Thread引用: 采用匿名内部类的形式创建Thread子类并向上转型为Thread,并使用定义好的Thread引用指向它 * 也可以将线程创建在方法内,当准备好运行线程时,就可以调用这个方法,而在线程 开始之后,该方法将返回 加入一个线程: 在A线程中调用B的B.join()方法 A线程会被挂起 直达目标线程B结束才恢复 在调用join()时带上一个超时参数(单位可以是秒,毫秒或纳秒) 这样如果目标线程在这段时间到期时还没有结束的话 join()方法总能返回 对join()方法的调用可以被中断 使用 B.interrupt()方法 异常捕获: 在线程中的run()方法内抛出的异常,并不会被外部线程捕获到 专用: Thread.UncaughtExceptionHandler接口 共享受限资源: 对于并发工作,需要某种方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种问题 采用:序列化访问共享资源 的方案: 在给定时刻只允许一个任务访问共享资源 共享资源一般是以对象形式存在的内存片段,但也可以是文件、输如/输出端口,或者打印机 要控制对共享资源的访问,得先把它包装进一个对象,然后把所有要访问这个资源的方法标记为synchronized synchronized void f(){} 所有对象都自动含有单一的锁(也称为监视器),当在对象上调用任意synchronized方法的时候,此对象都被加锁, 这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用 在并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会发生冲突 针对每个类,也有一个锁(作为Class对象的一部分),所以synchronized static方法可以在类的范围内防止对static数据的并发访问 什么时候使用 同步: 如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步 并且,读写线程都必须用相同的监视器锁同步 重要的一点:每个访问临界共享资源的方法都必须被同步,否则它们就不会正确的工作 显示的Lock对象: java.util.concurrent.locks中显示的互斥机制 Lock对象必须被显示的创建、锁定、释放 例子: public class MyClass{ private int currentValue = 0; private Lock lock = new ReentrantLock(); public int next(){ /*惯用写法 先加锁 在采用try实现对竞争资源的操作 最后finally语句进行解锁*/ lock.lock(); try{ ++currentValue; Thread.yield(); ++currentValue; return currentValue; }finally{ lock.unlock(); } } } 大体上,使用synchronized关键字时,需要写的代码量更少,并且用户错误出现的可能性也会降低,因此通常只有在解决特殊问题时,才使用显示的Lock对象 原子性与易变性: 原子操作是不能被线程调度机制中断的操作,一旦操作开始就一定会在 切换到其他线程执行 之前 执行完毕 原子性可以应用于除long和double之外的所有基本类型上的“简单操作” 对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存 易变性的关键字---volatile 如果将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改 volatile域会立即被写入主存中,读取操作就是发生在主存中 i++这样的操作在java中是非原子性的 基本上,如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么就应该将这个域设置为volatile的 读取和写入都是直接针对内存的,而却没有缓存, 但是volatile并不能对递增不是原子性操作这一事实产生影响: public class SerialNumberGenerator{ private static volatile int serialNumber = 0; public static int nextSerialNumber(){ return serialNumber++; //volatile不能阻止此操作是非原子性的 因此 此处是非线程安全的 } } 对基本类型的读取和赋值操作被认为是安全的原子性操作,当对象出于不稳定状态时,仍旧有可能使用原子性操作来访问它们: private int i = 0; public int getValue(){ return i; } public synchronized void iPlusOne(){ i++; i++; } 注:在某个线程中调用iPlusOne() 在另一个线程中调用getValue() 由于i++是原子性操作 而getValue()方法不是synchronized 导致获取到奇数的中间状态值 原子类: AtomicInteger AtomicLong 等等 Atomic类被设计用来构建java.util.concurrent中的类,因此只有在特殊情况下才在自己的代码中使用它们 临界区: 希望防止多个线程同时访问方法内部的 部分代码 而不是防止访问整个方法 --- 被分离出来的部分代码段被称为临界区(critical section) 使用sychronized关键字建立,synchronized被用来指定某个对象: synchronized(syncObject){ ...//临界区---同步控制块,进入此段代码前必须得到syncObject对象的锁 } 具体使用案例687页 还可以使用显示的Lock对象来创建临界区://689页的例子 class ExplicitPairManager2 extends PairManager{ private Lock lock = new ReentrantLock(); public void increment(){ Pair temp; lock.lock(); try{ p.incrementX(); p.incrementY(); }finally{ lock.unlock(); } store(temp); } } 在其他对象上同步: synchronized块最合理的使用方式是,使用其方法正在被调用的当前对象: synchronized(this) 这种方式下,如果获得了synchronized块上的锁,那么该对象其他的synchronized方法和临界区就不能被调用了 class DualSynch{ private Object syncObject = new Object(); public synchronized void f(){ for(int i = 0; i < 5; ++i){ System.out.println("f()"); Thread.yield(); } } public void g(){ synchronized(syncObject){ //同步的对象为另一Object对象 并不是this 这样可以保证两个同步是相互独立的 for(int i = 0; i < 5; ++i){ System.out.println("g()"); Thread.yield(); } } } } 线程本地存储://这样每个线程都会有自己的变量 不会发生冲突 也就不需要进行同步 创建和管理线程本地存储可以由 java.lang.ThreadLocal类来实现 public class ThreadLocalVariableHolder{ private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){ private Random rand = new Random(47); protected synchronized Integer initialValue(){ return rand.nextInt(10000); } };//在一个类内调用使用此类时,每个线程都会为变量分配自己单独的存储 } 注:ThreadLocal对象通常当作静态域存储 在创建ThreadLocal时,只能通过get()和set()方法来访问该对象的内容 无需使用synchronized 因为ThreadLocal保证不会出现竞争条件 终结任务: cancel()和isCanceled()方法被放到了一个所有任务都可以看到的类中 设置canceled变量人为终止任务: public void run(){ while(!canceled){ ... } } 线程状态: 一个线程可以处于以下四种状态之一: 1 新建(new):当线程被创建时,它只会暂时处于这种状态,此时已经分配了必需的系统资源,并执行了初始化 2 就绪(Runnable):只要调度器把时间片分配给现线程,线程就可以运行 3 阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间 4 死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,任务已经结束不再是可运行的 任务死亡的通常方式是从run()方法返回 进入阻塞状态的可能原因: 1 通过调用sleep()使任务进入休眠状态 2 通过调用wait()使线程挂起 直到线程得到了notify()或notifyAll()消息 (signal()或signalAll()消息) 线程会进入就绪状态 3 任务在等待某个输入/输出完成 4 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用(被另一个任务获取了锁) 中断: 如果一个线程已经阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出 InterruptedException Thread.interrupted() 使用Executor来执行操作时,调用Executor的shutdownNow()方法 它将发送一个interrupt()调用给它启动的 所有线程 要中断某个单一任务,则使用ExecutorService的submit()方法 代替execute()方法: submit()方法将返回一个泛型Future<?>,其中有一个未修饰的参数 --- 持有这种Future的关键字在于你可以在其上调用cancel()方法来中断某个特定任务 sleep引起的阻塞可中断 I/O以及synchronized引起的阻塞不可中断 关闭任务在其上发生阻塞的底层资源来中断阻塞: * exec.shutdownNow() * 直接关闭流 * 调用Future的cancel() 一个任务应该能够调用在同一个对象中的其他的synchronized方法,而这个任务已经持有锁了 //699页 中断检查: 将退出考虑全面:在阻塞时调用interrupt()方法抛出InterruptException进行catch处理 非此类情况则需要第二种处理方式来退出 //考虑被阻塞和非阻塞的各种情况 //结合使用try-finally一个典型的惯用法 701页 线程之间的协作: 使用wait() notify() notifyAll() 协作 //三种方法都是Object类的 它们操作的锁也是所有对象的一部分 wait()的调用有两种 1 以毫秒为单位的参数--表示挂起时间 2 没有参数--表示一直挂起--无限等待 在wait()期间对象锁被释放 调用notify() 或 notifyAll()可以是处于wait()的线程唤醒 notify():在众多等待同一个锁的任务中只有一个会被唤醒 notifyAll():唤醒多个处于wait()的任务 注意的是:wait() 和notify() 的使用针对的都是同一个对象 例如一个对象上使用notifyAll() 在另一个对象上的wait()的任务是不会被唤醒的 生产者与消费者: 一个很好的例子//709页 针对不同task对象设定不同的synchronized块(不同任务不同的锁) 当wait()被调用后,任务挂起(wait)并等待唤醒信号(notify/notifyAll) 再次强调:任务结束的方式---run()是如何返回的 使用显示的Lock和Condition: 使用Condition和Lock对象,单个Lock将产生一个Condition对象,这个对象被用来管理任务间的通信 但是Condition对象并不包含任何有关处理状态的信息,需要额外的处理信息 使用的方法:await()来挂起 <-> signal() signalAll()来唤醒 使用的套路: class Car{//被操作的对象类 里面定义必要域 以及 对象上的不同状态 private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private boolean waxOn = false; public void waxed(){ lock.lock(); try{ waxOn = true; condition.signalAll(); }finally{ lock.unlock(); } } //... } class WaxOn implements Runnable{ /*...*/ }//被操作对象上的具体操作任务 有Runnable编码套路while(!Thread.interrupted()){...} 注:使用Lock和Condition可能会使编码的复杂性变得更高但收益很少 --- Lock和Condition对象只有在更加困难的的多线程问题中才是必需的 生产者-消费者与队列: 解决:wait()和notifyAll()方式一种非常低级的方式解决了任务互操作问题,即每次交互时都握手 使用 同步队列 解决任务协作问题:java.util.concurrent.BlockingQueue接口 LinkedBlockingQueue ArrayBlockingQueue SynchronousQueue 经典例子://715页的土司 没有任何显示的同步(使用Lock对象或者synchronized关键字的同步) 同步由队列(其内部是同步的)和系统的设计隐式地管理 每一个被操作的对象在任何时刻都只由一个任务在操作 因为队列的阻塞,使得处理过程将被自动地挂起和恢复 任务间使用管道进行输入/输出://piped PipedWriter:允许任务向管道写 PipedReader:允许不同任务从同一管道中读取 PipedReader的建立必须在构造器中与一个PipedWriter相关联 使用 PipedReader in = new PipedReader(out); //其中out被定义为 PipedWriter out = new PipedWriter(); 注意:BlockingQueue使用起来更加健壮而容易 死锁: 任务循环等待下一个任务释放锁 必须同时满足四个条件才会发生死锁: 1 互斥条件 任务使用的资源中至少有一个是不能共享的 2 至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源 3 资源不能被任务抢占 任务必须把资源释放当作普通事件 4 必须有循环等待 新类库中的构建: java.util.concurrent库引入大量设计用来解决并发问题的的新类 //用到时再仔细研究 CountDownLatch:被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成 CyclicBarrier:适用于:创建一组任务,它们并行地执行工作,然后在进行下一步骤之前等待,直至所有任务都完成(/*赛马*/) DelayQueue;延迟队列 PriorityBlockingQueue:优先级队列 ScheduledExecutor:预定义工具 ScheduledThreadPoolExcutor Semaphore:计数信号量允许n个任务同时访问一个资源 Exchanger:在两个任务之间交换对象的栅栏 典型应用场景:一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务在消费这些对象 可以使得 有更多的对象在被创建的同时被消耗 当一个任务调用其Exchanger.exchange()方法时,它将阻塞直至对方任务调用它自己的exchange()方法,那时,这两个exchange()方法将全部完成 对象被互换 性能优化: synchronized Lock Atomic 如果涉及多个Atomic对象,就有可能被强制要求放弃这种用法: Atomic对象只有在非常简单的情况下才有用//754页 免锁容器: 免锁容器背后的通用策略:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可 修改是在容器数据结构的某个部分的一个单独的副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的, 只有当修改完成时,被修改的结构才会自动地与主结构进行交换,之后读取者就可以看到这个修改了 CopyOnWriteArrayList CopyOnWriteArraySet ConcurrentHashMap ConcurrentLinkedDeque ReadWriteLock:对数据结构相对不频繁的写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化 仅允许一个写入者持有锁,允许多个读取者同时读取 活动对象:每个对象都维护着它自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队,任何时刻都只能运行其中的一个 有了活动对象://这段话我是没咋明白意思 1 每个对象都可以拥有自己的工作器线程 2 每个对象都将维护对它自己的域的全部控制权 3 所有在活动对象之间的通信都将以在这些对象之间的消息形式发生 4 活动对象之间的所有消息都要排队 总结: 使用多线程的主要原因: * 要处理很多任务,它们交织在一起,应用并发能够更有效地使用计算机(包括在多个CPU上透明地分配任务的能力)//等待输入/输出时使用CPU * 要能够更好地组织代码//仿真 * 要更便于用户使用//长时间下载过程中监视“停止”按钮是否被按下 多线程的主要缺陷: 1 等待共享资源的时候性能降低 2 需要处理线程的额外CPU花费 3 糟糕的程序设计导致不必要的复杂度 4 有可能产生一些病态,如 饿死 竞争 死锁 活锁 5 不同平台导致的不一致 ---------------------------------------------------------- 常见任务写法: public class Task implements Runnable{ //... public void run(){ try{ while(!Thread.interrupted()){ //do something } }catch(InterruptedException e){ //... } } } 使用synchronized方法是最常用的方法
--------------------------------------------------------笔记底线------------------------------------------------------------------