大道至简,知行合一。

《head first Java》笔记

重拾Java,进军大数据!

  • Java有3中循环结构:while循环、do-while循环和for循环。
  • Java编译器和虚拟机
    • 编译器:类型安全性检查,但是有些类型错误会在运行时发生,这是为了要容许动态绑定这样的功能,Java可以在执行期引用连程序员也没有预期会碰到的类型。安全的第一道防线,数据类型错误处理,如违反调用private方法;
    • 虚拟机:存取权限的安全;
  • main()的两种用途
    • 测试真正的类;
    • 启动Java应用程序;
  • 创建对象时,它会存储在堆空间中,此区域并非普通的堆,而是可回收垃圾的堆(Garbage-Collectible Heap)。Java会根据对象的大小来分配内存空间。Java会主动进行内存管理,当某个对象被Java虚拟机察觉不再会被使用到,该对象就会被标记成可回收的。如果内存开始不足,垃圾收集器就会启动来清理垃圾、回收空间,让空间能够再次被利用。
  • 在Java的面向对象概念中没有全局变量的概念,但实际上任何变量只要加上public、static和final,基本上都会变成全局变量取用的常数。但受限于Java面向对象的信条,任何Java中的事物都必须呆在类中,这种近似全局的事物在Java中算是例外,是非常特殊的情况,不会有多个实例或对象。
  • Java程序是由一组类所组成,其中一个类会带有启动用的main()方法,程序员需要开发一或多个类并提交Java虚拟机运行。如果类文件数量庞大,可以把所有文件包装进依据pkzip格式存档的Java Archive即jar文件,在jar文件中可以引入一个简单文字格式的文字文件,被称为manifest,里面定义了jar中哪个文件带有启动应用程序的main()方法。
  • Java中变量有两种类型:primitive主数据类型和对象引用,primitive主数据类型用来保存基本类型的值,包括整数、布尔和浮点数等。变量必须拥有类型和名称,如”int count”,int是类型,count是名称。
  • float和double区别:float占4个字节,double占8个字节,double 和 float 的区别是double精度高,有效数字16位,float精度7位(可提供7位或8位有效数字,构成包括符号位、指数位和尾数位),但double消耗内存是float的两倍,double的运算速度比float慢得多,能用单精度时不要用双精度。
  • 变量赋值时要重点考虑类型是否足够装载问题,否则会发生溢位,编译器会试着防止这种情况发生。需要注意的是,编译器不允许将大类型的内容放到小类型中,但是小的可以放到大的中,另外大的放入小的,其实是可以的,只是会损失某些信息。
  • 事实上没有对象变量这样的东西,只有引用(reference)到对象的变量,对象只会存在于可回收垃圾的堆。对象引用变量保存的是存取对象的方法,它不是对象的容器,而是类似指向对象的指针,或者说是地址。但在Java中我们不会也不该知道引用变量中实际装载的是什么,它只是用来代表单一的对象,只有Java虚拟机才会知道如何使用引用来获取对象。primitive主数据类型变量是以字节来表示实际的变量值,但对象引用变量却是以字节来表示取得对象的方法。
  • 对象的声明、创建和赋值有3个步骤:Dog myDog = new Dog();
    • 声明一个引用变量:Dog myDog,要求Java虚拟机分配空间给引用变量,即申请一个放遥控器的的杯子;
    • 创建对象:new Dog(),要求Java虚拟机分配堆空间给新建立的Dog对象;
    • 连接对象和引用:即赋值,将遥控器关联Dog对象,而且Dog遥控器可以重新引用另一个Dog,如果不赋值也就是不引用任何东西就会是null;
  • 对于任意一个Java虚拟机来说,所有的引用大小都一样,但不同的Java虚拟机间可能会以不同的方式来表示引用,因此某个Java虚拟机的引用大小可能会大于或小于另一个Java虚拟机的引用。
  • 数组犹如杯架,其中的每个元素都是变量,会是8中primitive主数据类型变量中的一种或者引用变量。同时,无论承载哪种数据类型或对象引用,数组本身永远是对象。
  • Java的标准函数库包含了很多复杂的数据结构,比如map、tree和set,但如果需要快速、有序、有效率地排列元素时,数组是不错的选择。数组能够让你使用位置索引来快速、随机地存取其中的元素。
  • 实参到形参的赋值,Java是通过值传递,即通过拷贝传递。这里的值是指变量所携带的值,引用对象的变量携带的是远程控制而不是对象本身。
  • Java中方法只能声明单一的返回值,如果需要返回多个值,可以打包到数组中,如果混合不同类型的值返回,可以使用ArrayList。
  • 传入和传出方法的值类型可以隐含地放大或是明确地缩小。
  • 实例变量永远都会有默认值,如果没有明确地赋值给实例变量,或者没有调用setter,实例变量还是会有值。其中integers:0、floating points:0.0、booleans:false、references:null。
  • 实例变量声明在类中,而局部变量声明在方法中。相比较实例变量,局部变量在使用前必须初始化,否则编译器会报错,原因在于局部变量没有默认值。
  • 方法的参数类似局部变量,都在方法中声明,确切地说是在方法的参数列声明,但相较于实例变量它也算是局部的。而参数并没有未声明的问题,所以编译器也不可能显示出错误。原因在于调用方法而如果没有赋值参数时,编译器就会报错,所以参数一定会被初始化,编译器会确保方法被调用时会有与声明所相符的参数,且参数会自动地被赋值进去。
  • 使用==来比较两个primitive主数据类型,或者判断两个引用是否引用同一个对象。使用equals()来判断两个对象是否在意义上相等,如两个String对象是否带有相同的字节组合。
  • For循环
    • for (int i=0; i<10; i++):基础版for循环;
    • for (int cell: locationCells):从Java 5.0开始可以对数组或其他集合使用加强版for循环;
  • ArrayList无法保存primitive主数据类型,如果确实需要的话可以使用主数据类型的包装类,甚至在Java 5.0版本上,包装工作会自动进行。相比较数组,在运用primitive主数据类型时ArrayList的速度和效率要低一些。
  • ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
  • ArrayList从Java 5.0开始有特殊的东西就是参数化类型,即泛型,形如:ArrayList<String> 或 ArrayList<Dog>,但这里generic类型的规则时只能指定类或接口类型,无法使用primitive主数据类型;
ArrayList<String> myList = new ArrayList<String> (); 				// ArrayList初始化
Arraylist myList01 = new ArryaList();		//会报错,未声明类型
String[] myList = new String[2];														// 一般数组初始化

// ArrayList的方法
add()				//增加元素
remove()		//删除元素
indexOf()		//查找元素位置
isEmpty()		//判断是否为空
size()			//查询大小
  • 要使用Java API中的类,就必须知道它被放在哪个包中:
    • javax.swing:Swing接口类;
    • java.util:ArrayList等工具类;
    • java.lang:System、String、Math等,除了这个包,其他类在程序中使用必须指明其完整名称,即需要导包或者全名;
  • 认识import
    • Java中的import和C语言的include并不相同,程序不会因为使用import而变大变小;
    • java.lang是经常使用到的基础包,会被预先引用,类似java.lang.String和java.lang.System都是独一无二的class,Java知道如何寻找;
  • 继承
    • 在C++中有多继承,即一个派生类可以有两个或多个基类,但Java、C#、PHP等都只有单继承,这里的单继承指的是多层继承,多继承指多重继承;
    • Java继承中,子类对象实例调用方法时,会从与该对象类型最接近的方法,即Java虚拟机会从最低阶逐步向上搜索,直到找到为止;
    • 继承的方法可以覆盖,但实例变量不能被覆盖;
    • 继承应该遵循”IS-A”的原则,即”是一个”和”有一个”的区别,而且继承层次树的任何地方都应该遵循,即所有子类都应该通过任一上层父类的”IS-A”测试;
    • 存取权限和修饰符:通常只会用到public和private两种,protected和default权限等级都是作用在包上。
      • public:会被继承,子类会把成员当成自己定义的一样,同时成员是公开的,所有其他类都可以访问;
      • protected:受保护权限,体现在继承,即子类可以访问父类受保护对象,同时相同包内的其他类可以访问,换言之,只比default多了一个允许不同包的子类继承它的成员;
      • default:默认,包访问权限,同一个包内可以访问;
      • private:私有的,不会被继承,只有本身可以访问,只有同一个类中的程序代码可以存取,是对类而不是对象设限;
    • 继承会确保某个父型下的所有类都会有父型所持有的全部方法(全部可继承方法,且是public方法),即通过继承定义相关类间的共同协议;
    • liskov替换原则:子类对象可以被当成父类对象使用,如果降低子类对象的访问权限,就违反了这一原则。
      • Java子类覆盖父类方法时,不可以降低方法的访问权限,即子类继承父类的访问修饰符要与父类的相同或更开放;
  • 多态:通过声明为父型类型的对象引用来引用它的子型对象,但只能调用父类中声明的方法;
    • 编译器会寻找引用类型来决定是否可以调用该引用的特定方法;
    • 执行期,Java虚拟机寻找的并不是引用所指的类型,而是堆上的对象;
    • 编译器是根据引用类型来判断哪些方法可以调用,而不是根据对象确实的类型;
  • 重载:两个方法名称相同,但是参数不同,返回类型不做区分;
  • 抽象
    • 抽象类:有些类从设计和逻辑上就不应该被初始化,即不能”new”出来,只有其子类可以被初始化,通过”abstract”关键字标记为抽象类,编译器就知道无论在哪,这个类都不能创建任何类型的实例。
      • 虽然抽象类不能实例化,但是仍然可以使用其声明为引用类型给多态使用;
      • 抽象类除了被继承之外,除了抽象类可以有static成员外,是没有用途、没有值、没有目的的;
      • 非抽象类就是具体类;
    • 抽象方法:标记为abstract的方法,抽象方法没有实体;
    • 抽象类必须要被继承;抽象方法必须要被覆盖,拥有抽象方法的类必须是抽象类;
    • 将可继承的方法体(有内容的方法)放在父类中是个好主意,但有时就是没有办法做出给任何子类都有意义的共同程序代码;抽象方法的意义是就算无法实现出方法的内容,但还是可以定义出一组子型共同的协议。抽象的意义就是为了使用多态
    • 抽象的方法没有内容,只是为了标记出多态而存在,表示在继承树结构下的第一个具体类必须要实现出所有的抽象方法,即使用相同的函数签名(名称和参数)和相同的返回类型创建出非抽象的方法;
  • Java中终极对象是Object类,所有类的父类,Java为这个终极对象设计了很多对象通用行为,包括:
    • equals(Object o):判断两个对象是否相等;
    • getClass():查询对象从哪里初始化的;
    • hashCode():列出对象的哈希代码;
    • toString():列出类的名称和一个不需关心的数字;
  • 任何从ArrayList<Object>取出的东西都会被当做Object类型的引用而不管它原来是什么,编译器无法将次对象识别为Object以外的事物,即放进去的任何类型对象出来都变成了Object,参考下面的包装类操作。
    • 同理在函数传参和赋值时也有这个问题,当一个对象被声明为Object类型的对象所引用时,它无法再赋值给原来类型的变量;
    • 如果要转换为原本的类型,需用进行类型转换,这里需用自行确认其真实类型,如果不能确定类型,可以使用”instanceof”运算符来检查,而如果堆上的对象类型和所要转换的类型不兼容,则执行期会产生异常;
  • 考虑到”致命方块”问题,子类多个父类中有相同函数,向上查找无法确认使用哪个版本,为了简单化,Java不允许多重继承;
  • 接口:所有方法都是抽象的,子类必须进行实现,就像100%的纯抽象类,可以用来解决多重继承问题却不会产生致命方块问题。
// 接口定义:接口方法一定是抽象的,所有必须以分号结束,没有方法体,访问修饰符(public abstract)是选择性,可以不写,因为接口里面的方法都是必须要由实现类去实现的,即使在写的时候不加,在编译之后生成 class 文件时,都会自动加上。
public interface Pet {
	public abstract void beFriendly();
  public abstract void play();
}	

// 继承接口:必须实现继承的接口
public class Dog extends Canine implements Pet {
	public void beFriendly() {}
  public void play() {}
}		
  • 抽象类、接口、继承等等设计都是为多态准备的
    • Java中可以使用接口引用任何实现该接口的东西,因此可以为不同的需求组合不同的继承层次;
    • 不同继承树的类可以实现相同的接口,一个类可以实现多个接口;
    • 使用类作为多态类型运用时,相同的类型必须来自同一个继承树,而且必须是该多态类型的子类,但使用接口作为多态类型时,对象可以来自任何地方,唯一的条件就是该对象必须来自有实现此接口的类;

如何设计类、子类、抽象类和接口?

  • 如果新的类无法对其他类通过”IS-A”测试,就设计新的类,而不是继承其他类;
  • 只有在需要某类的特殊化版本时,以覆盖或增加新的方法来继承现有的类;
  • 需要定义一群子类的模板,又不想初始化该模板时,可以设计为抽象类;
  • 如果想要定义出类可以扮演的角色,使用接口;
  • 如果创建出的一个具体的子类必须要覆盖某个方法,但又需要执行父类的方法,即不打算完全覆盖掉原来的方法,只是加入额外的动作,可以使用”super”关键字调用父类的方法。
  • Java中,程序员会在乎内存中的两种区域
    • 堆(heap):对象的生存空间;
    • 栈(stack):方法调用和局部变量的生存空间;
    Java虚拟机启动时,会从底层操作系统申请一块内存,并以此区段执行Java程序,内存大小以及是否可以调整与Java虚拟机和平台版本有关,通常无法控制。
  • 构造函数
    • 构造函数和类名相同,与普通方法的差异在于其没有返回值,构造方法不会被继承(?),普通方法也可以和类名相同;
    • 构造函数的一个关键特征是会在对象能够被赋值给引用之前就执行,用户有机会在对象被使用之前介入;
    • 如果类中没有定义无参构造方法,那么编译器会自动添加无参构造方法,但是如果编写时添加了有参构造方法而未添加无参构造方法,那么编译器只认有参构造方法而不会默认添加无参构造方法,所以,如果需要使用无参构造方法,一定要在类里面添加;
    • 有多个构造函数时,参数一定要不一样,包括参数顺序和类型;
    • 构造函数可以是公有、私有或不指定的,私有的构造函数不是完全不能存取,而是代表该类以外不能存取;
    • 在创建新对象时,所有继承下来的构造函数都会执行。执行new的指令是重大事件,它会启动构造函数连锁反应(构造函数链),就算是抽象类也有构造函数,虽然不能对抽象类执行new操作,但是抽象类作为父类,其构造函数会在具体子类创建实例时执行。这里父类构造函数必须被执行的设计原因是子类可能会根据父类的状态(实例变量)来继承方法。构造函数被执行时,会第一时间执行父类构造函数,“先有父母后有孩子”。
    • 调用父类的构造函数的方法是”super()”,如果没有主动调用编译器会自动添加,情况还分为两种:
      • 没有编写子类构造函数:会在自动添加的构造函数中调用父类构造函数;
      • 有编写子类构造函数但没有调用super():编译器会对每个重载的构造函数添加super(),但是需要注意,如果父类有多个重载版本的构造函数,编译器自动添加的一定是无参版本;
      父类构造函数super()的调用必须是子类构造函数的第一个语句。
    • 使用this()从某个构造函数调用同一个类的另外一个构造函数,this是对对象本身的引用,this()只能用在构造函数中,且必须是第一行语句,super()和this()不能共存;
  • 对象的唯一引用消失后,对象就会变成可回收的,符合垃圾回收器(GC)的条件,如果程序内存不足,GC就会回收部分或全部可回收对象空间。
  • 释放对象引用的方法有三种:
    • 引用永久性离开了它的作用域,如函数内部的局部对象引用在函数执行完毕后就会消失;
    • 引用被重新复制给其他对象;
    • 直接将引用设定为null;
  • null代表”空”的字节组合,实际上只有Java虚拟机才会知道是什么。
  • Math这个类中所有方法都不需要实例变量值,因为这些方法都是静态的,所以无需Math的实例,只需类本身。
  • 静态方法:Java是面向对象的,但某些特殊情况下,通常是实用方法,不需要类的实例,可以用static这个关键字标记,通俗来讲,静态方法就是一种不依靠实例变量也就不需要对象的行为。带有静态方法的类通常不打算要被初始化,但并不是有一个或多个静态方法的类就不能被初始化,事实上有main()的类都算有静态方法。
  • 静态方法只能访问类中静态成员,不能调用非静态的变量,也不能调用非静态的方法,原因都在于非静态变量和方法都是关联实例对象的状态和行为,而静态方法不打算实例化。非静态成员则可以访问类中所有成员。
  • 虽然可以使用对象实例代替类名来调用静态方法,但是合法并不一定都是好事,会产生误解,且编译器还是会解析为原来的类。
  • 限制类被初始化的方法
    • 抽象类:使用abstract关键字修饰;
    • 使用私有的构造函数来限制非抽象类被初始化,如类中只有静态方法,可以这样避免初始化;
  • 取得新对象的方法只有通过new或者序列化以及反射(Java Reflection API)。
  • 静态变量:被同类的所有实例共享的变量,只会在类第一次载入的时候被初始化,实例对象中不会维护静态变量的拷贝。Java虚拟机加载类是因为尝试要创建该类的实例或使用该类的静态方法或变量,这保证了:静态变量会在该类任何对象创建之前、任何静态方法执行之前就完成初始化。静态变量比较有效率,共享的类会省下很多内存。
  • 静态变量不是全局变量,而是面向对象化的类中,是对象的自然状态,唯一的差别是被很有效率地共享。但是不能滥用静态变量,那与面向对象的思想相背离,应该关注对象的状态而不是类的状态。
  • 静态的final变量是常数(常量,即用static和finall修饰的变量),一旦被初始化就不会被改动,常数变量的名称都需要是大写字母。
  • 静态final变量的初始化方式有两种,如果未采用任一种赋值编译器会报错
    • 声明的时候即赋值:public static final int X = 10;
    • 在静态初始化程序中(一段在加载类时会执行的程序代码,它会在其他程序可以使用该类前就执行,所以很适合放静态final变量的起始程序)
public class Bar {
  public static final double BAR_SIGN;
  static {			// 静态初始化程序
    BAR_SIGN = (double)Math.random();
  }
}
  • 执行顺序:父类静态初始化程序 -> 子类静态初始化程序 -> 父类构造函数 -> 子类构造函数;
  • final关键字:代表不能变动,防止变量值修改、方法覆盖或创建子类等,变量被初始化赋值后不能修改;
class A {
  final int size = 3;			// size将无法更改
  final int b;
  A() {
    b = 2;			// b将无法更改
  }
  void doStuff(final int x) {
    // x将不能更改
  }
  void doMore() {
    final int z = 7;	//z将不能更改
  }
  final vod doSome() {}		//doSome将不能被覆盖
}
final class B {}		// B将不能被继承
  • Math常用方法
    • Math.random():返回介于0.0~1.0之间的双精度浮点数;
    • Math.abs():返回双精度浮点数类型参数的绝对值,因为该方法有重载版本,所以传入整型返回整型,传入双精度浮点返回双精度浮点数;
    • Math.round():根据参数是浮点型或双精度浮点数返回四舍五入之后的整型或长整型值;
    • Math.min():返回两参数中较小的那个;
    • Math.max():返回两参数中较大的那个;
  • 包装类:Java 5.0之前的版本,primitive主数据类型无法放入ArrayList或HashMap中,需要使用对应的包装类,这些包装类都在java.lang包中,所以无需imort。5.0版本后Java加入了autoboxing功能能够自动将primitive主数据类型转换成包装类对象。
// 包装类
Boolean
Character
Byte
Short
Integer
Long
Float
Double

//包装
int i = 10;
Integer iWrap = new Integer(i);		//传递primitive主数据类型给包装类的构造函数

//拆包
int unWrapped = iWrap.intValue();

//无autoboxing的ArrayList操作
ArrayList numberList = new ArrayList();				//创建ArrayList对象
numberList.add(new Integer(3));
Integer one = (Integer) numberList.get(0);		//返回都是Object类型,但可以转换成Integer类型

//有autoboxing的ArrayList操作
ArrayList<Integer> listOfNumbers = new ArrayList<Integer> ();		//创建Integer类型的ArrayList对象
listOfNumbers.add(3);								//编译器会自动将int包装成Integer
int num = listOfNumbers.get(0);			//编译器会自动将Integer拆包

//其他使用autoboxing的地方
void takeNumber(Integer i) {}			//方法参数如果是包装类,可以传递primitive主数据类型,反之亦然
int giveNumber() { return x; }		//方法返回值
if(bool) {}												//boolean表达式,boolean表达式或Boolean包装类
Integer i = new Integer(10);
i++;															//数值运算
Double d = i;											//赋值

//包装类静态的实用性方法
Integer.parseInt("2")
Double.parseDouble("10.02")
new Boolean("true").booleanValue()		//没有parseBoolean,而Boolean构造函数可以使用字符串来创建对象

//primitive主数据类型转换String
double d = 0.02;
String doubleString = "" + d;				//"+"是Java中唯一重载过的运算符
String doubleString2 = Double.toString(d);

数字格式化:Java中数字和日期的格式化功能没有结合在输出/输入功能上,在5.0之前格式化功能是通过java.text这个包来处理,之后通过java.util中的Formatter类来提供,但无需创建和调用该类方法,因为Java已经把这些便利功能加到部分输出/输入类和String上了,因此只需要调用静态的String.format()并传入值和格式设定就行。

//format第一个参数被称为“格式化串”,它可以带有实际上就是这么输出而不用转移的字符,'%'符号表示把后面的参数放这里,是会被方法其余参数替换掉的位置
String.format("%, d", 10000);																//格式是',d'
String.format("I have %.2f bugs to fix.", 123.56);					//格式是'.2f'
String.format("The rank is %,d out of %,.2f", 100, 200.02)	//多个参数会依序对应到格式化设定,Java使用可变参数列表提供了该API

//格式化语法,type包括d-decimal,f-floating point,x-hexadecimal,c-charcater
%[argument number][flags][width][.precision]type
  • 日期格式化:与数字格式化的主要差别在于日期格式的类型时用”t”开头的两个字符表示
Date today = new Date();
String.format("%tc", today);															//完整的日期与时间, Sun Nov 28 14:00:01 MST 2020
String.format("%tr", today);															//只有时间,03:01:48 PM
String.format("%tA, %tB %td", today, today, today);				//周、月、日, Sunday, November 28
String.format("%tA, %<tB %<td", today);										//同上,但重复利用参数
  • 要取得当前日期时间使用java.util.Date,其他操作日期的功能使用java.util.Calendar。
Calendar c = Calendar.getInstance();				//Calendar是个抽象类,无法new出实例,只能调用getInstance这个静态方法获得Calendar的具体子类实例,大部分Java版本会默认返回一个java.util.Gregorian-Calendar的实例
c.set(2020, 2, 2, 15, 20);									//设置时间,注意月份以0位基准
long d1 = c.getTimeInMillis();							//将当前时间转换为已微秒表示,是相对于1970年1月1日的微秒数
d1 += 1000 * 60 * 60
c.setTimeInMillis(d1);											//添加一个小时
System.out.println(c.get(c.HOUR_OF_DAY));		//打印小时数
c.add(c.DATE, 35);													//添加35天
c.roll(c.DATE, 35);													//滚动35天,只有日期字段滚动,月份不会动
c.set(c.DATE, 1);														//直接设置DATE的值
System.out.println(c.getTime());						//获取时间
  • 静态import:import静态的类、变量或enum能够少打点字,但是可能会让程序可读性降低,且容易产生重名冲突。
//常规写法
import java.lang.Math;
class NoStatic {
  public static void main(String[] args) {
    System.out.println(Math.sqrt(2.0));
    System.out.println(Math.tan(60));
  }
}

//使用static import的写法
import static java.lang.System.out;
import static java.lang.Math.*;
class WithStatic {
  public static void main(String[] args) {
    out.println(sqrt(2.0);
    out.println(tan(60));
  }
}
  • Java的异常处理
//1.有风险的程序代码需要声明可能抛出的异常
public void takeRisk() throws BadException {
  if (abandonAllHope) {
    throw new BadException();
  }
}

//2.调用风险程序使用try/catch语句块处理异常
public void riskHandle() {
  try {
  	anObject.takeRisk();
	} catch(BadException ex) {					//建议根据异常处理的逻辑捕获不同的异常,不要全都Exception,否则无法定位异常点,且根据异常类型采用不同的处理方案
  	System.out.println("BadException");
    ex.printStackTrace();
	} catch(Exception ex) {							//多个异常要从小到大按照继承层次排列
    System.out.println("Exception");
  }
  finally {
    System.out.println("收尾工作");			//try或catch块有return指令,流程会跳到finally然后回到return指令
  }
}

//3.如果未使用try/catch语句块捕获并处理异常,需要duck掉异常,让上层处理
  • Exception是所有异常的父类,包括InterrupedException和RuntimeException,也就是说它可以catch所有异常,包括期间(unchecked)的异常,因此不应该用在测试以外的环境中。
  • 区分throw和throws
    • throw是在程序中明确引发异常,出现在方法体中,用于抛出异常。当方法在执行过程中遇到异常情况时,将异常信息封装为异常对象,然后throw。
    • throws的作用是如果一个方法可以引发异常,而它本身并不对该异常处理,那么它必须将这个异常抛给调用它的方法。出现在方法的声明中,表示该方法可能会抛出的异常,允许throws后面跟着多个异常类型。
  • Java中异常分为’受检查异常’和’不受检查的异常’两种,不是由编译器检查的RuntimeException的子类被称为检查异常,而RuntimeExceptiion被称为不检查异常,不需要声明或包在try/catch语句块中,虽然可以自行抛出但是没必要,编译器也不管。
  • duck掉异常:catch捕获到的异常,如果不打算处理,可以正式地将异常给duck掉来通过编译。逻辑就是调用危险方法时,编译器需要我们对危险方法产生的异常进行处理,即使用try/catch语句块,也可以采用另一种方案:duck掉异常让上层逻辑处理,这需要将可能捕获的异常在方法声明中标记出来,这就能通过编译。如果踢皮球到最后main()也duck掉异常,Java虚拟机会死。
  • 异常处理规则
    • try一定要有catch或finally;
    • 只有finally的try必须要声明异常,即没有catch就要duck;
void go() throws FooException {
  try {
    x.doStuff();
  } finally { }
}
  • 使用命令行参数
public class CmdLine {
  public static void main(String[] args) {
    if (args.length < 2) {
      System.out.println("length too short.");
    } else {
      System.out.println(Integer.parseInt(args[1]));
    }
  }
}
  • 内部类:内部类可以使用外部所有方法和变量,即使是私有的,这就是内部类好用的原因,除了和正常的类没有差别外,还多了特殊的存取权限。内部类的实例一定会绑在外部类的实例上,只能存取绑定的外部类实例的方法和变量,这两个对象在堆上有特殊的关系,内外可以交互使用变量。非常特殊的异常情况:内部类定义在静态方法中,很少遇到,不讨论。
//外部类程序代码中初始化内部类,内部对象会绑在外部对象上.
class MyOuter {
  private int x;
  MyInner inner = new MyInner();
  public void doStuff() {
    inner.go();
  }
  class MyInner{
    void go() {
      x = 42;
    }
  }
}

//外部类以外初始化内部类实例,需要使用特殊的语法,通常不这么做.
MyOuter outerObj = new MyOuter();
MyOuter.MyInner innerObj = outerObj.new myInner();

序列化

  • 对象被序列化时,被该对象引用的实例变量也会被序列化,且所有被引用的对象的也会被序列化,这都是自动进行的;
  • 如果要让类能够被序列化,就需要实现Serializable接口(marker或tag类的标记用接口),该接口并没有任何方法需要实现,唯一目的就是声明有实现它的类时可以被序列化的,即此类对象是可以通过序列化机制来存储,且某类是可序列化的,则子类也自动地可以序列化,接口的本意就是这样;
  • 序列化要求对象内所有内容都可以序列化,否则无法进行。如果某实例变量不能或不应该被序列化,就把它标记为transient(瞬时)的,被标记的变量就是不需要序列化的,序列化程序会把它跳过,transient的引用实例变量会以null返回,而不管存储当时它的值是什么;
import java.io.*;

public class Box implements Serializable {
  private int width;
  private int height;
  
  public void setWidth(int w) {
    width = w;
  }
  
  public void setHeight(int h) {
    height = h;
  }
  
  public static void main(String[] args) {
    Box myBox = new Box();
		myBox.setWidth(10);
		myBox.setHeight(20);
    try {
      //创建FileOutputStream链接到ObjectOutputStream以让它写入对象
      FileOutputStream fs = new FileOutputStream("foo.ser");		//文件不存在会自动创建出来
      ObjectOutputStream os = new ObjectOutputStream(fs);
      os.writeObject(myFoo);		//写入参数都必须要实现序列化
      os.close();
    } catch(Exception ex) {
      ex.printStackTrace();
    }
  }
}
  • Stream(这里指串流,不是Java 8 新特性)是连接串流或是链接用的串流,连接串流用来表示源或目的地、文件、网络套接字连接,链接用串流来衔接连接串流。
  • java.io包输入/输出的API使用一种模块化的”链接”概念,用户可以把连接串流和链接串流以各种可能应用到的排列组合连起来,并不只是能衔接两种,可以多种链接串流。
    • 绝大多数场景只需要用到少数几个类,文本文件操作常用的BufferedReader和BufferedWriter、链接到FileReader和FileWriter就够了;
    • Java 1.4新增输入/输出nio这个类,效能提升并可以充分利用执行程序的机器的原始容量。可以使用FileInputStream的getChannel()方法来存取channel来使用nio。nio的主要作用在于能直接控制buffer和non-blocking的输入/输出,能让输入/输出程序代码在没有东西可以读取或写入时不必等在那里,但使用起来更复杂,需要小心设计,否则会效能损失,九成以上的应用不需要考虑。
  • 序列化和解序列化需要注意的是,输入/输出相关的操作都必须包在try/catch语句块中。
  • 解序列化(Deserialization):还原对象
    • 反序列化,在序列化后,在不同的Java虚拟机执行期,甚至不是同一个Java虚拟机,把对象恢复到存储时的状态;
    • 对象被解序列化时,Java虚拟机会通过尝试在堆上创建新的对象,让它维持与被序列化时有相同的状态来恢复对象的原状,这不包括transient的变量,它们如果是对象引用则是null,否则就是primitive主数据类型的默认值;
    • 解序列化时,所有的类都必须能让Java虚拟机找到;
    • 过程:对象从stream中读出,Java虚拟机通过存储的信息判断出对象的class类型,尝试寻找和加载对象的类,如果找不到或无法加载就会抛出异常,新的对象会被配置在堆上,但构造函数不会执行,否则会把对象的状态抹去变成全新的,而不是恢复存储时状态,如果对象在继承树上有个不可序列化的祖先类,则该不可序列化类和它之上的类的构造函数(就算是可序列化也一样)就会执行,一旦构造函数连锁启动就无法停止,即从第一个不可序列化的父类开始,全部重新初始化状态,对象的实例变量会被还原成序列化时点的状态值,transient变量会赋值为null或默认值;
    • 静态变量不会被序列化,因为静态变量是”每个类一个”,当对象被还原时,静态变量会维持类中原本的样子,而不是存储时的样子;
FileInputStream fileStream = new FileInputStream("MyGame.ser");
ObjectInputSteam os = new ObjectInputSteam(fileStream);
Object one = os.readObject();	//每次调用readObject()都会从stream读出下一个对象,读取顺序和写入顺序相同,次数超过会抛出异常
GameCharacter elf = (GameCharacter) one;	//返回值是Object类型,所以需要转换类型
os.close();
  • 反序列化的顺序与序列化时的顺序一致。
  • serialVersionUID:对象被序列化后,该对象会拥有的一个类的版本识别ID。解序列化时,Java虚拟机会比对serialVersionUID,版本不同会抛出异常。解决方案是在类中自行维护一个serialVersionUID,后续在修改类的时候不要有损害序列化的操作,比如之前版本的对象解序列化后新加入变量要有默认值。
$ serialver Dog			//使用JDK中的serialver工具获取类的版本ID
public class Dog {
  static final long serialVersionUID = 123456789000000L;
}

文件操作:java.io.File

  • File对象代表磁盘上文件或目录的路径名称,但不能读取或代表文件中的数据,是地址而不是房子;
  • 用到String文件名的串流大部分都可以用File对象来代替String;
  • 缓冲区:缓存写入数据,等到存满的时候再写入磁盘,减少对磁盘的操作次数,提高效率。可以使用writer.flush()强制缓冲区立即写入;
//File对象常用操作
File f = new File("abc.txt");		//创建出代表存盘文件的File对象
File dir = new File("Chapter1");
dir.mkdir();										//创建新的目录
if (dir.isDirectory()) {
  String[] dirContents = dir.list();
  for (int i = 0; i < dirContents.length; i++) {
    System.out.println(dirContents[i]);				//列出目录下的内容
  }
}
dir.getAbsolutePath();			//列出文件或目录的绝对路径
boolean isDeleted = f.delete();			//删除文件或目录,成果返回true

//读取文本文件
import java.io.*;
class ReadAFile {
  public static void main(String[] args) {
    try {
      File myFile = new File("MyTest.txt");
      FileReader fileReader = new FileReader(myFile);		//字符连接到文本文件的串流
      BufferedReader reader = new BufferedReader(fileReader);		//将FileReader链接到BufferedReader以获取更高的效率,它只会在缓冲区读空的时候才会读取磁盘
      String line = null;
      while ((line = reader.readLine()) != null) {		//最常见的数据读取方式,几乎所有非序列化对象都这样读
        System.out.println(line);
      }
      reader.close();
    } catch(Exception ex) {
      ex.printStackTrace();
    }
  }
}
  • Java的好处在于大部分输入/输出工作并不在乎链接串流的上游实际是什么,可以使用BufferedReader而不管串流来自文件或Socket。
  • Java的字符串分割
String s = "ABC/DEF";
String[] res = s.split("/");
for(String r: res) {
  System.out.println(r);
}
  • Java使用BufferedReader从Socket读取数据,用PrintWriter写数据到Socket
import java.net.*;		//Socket在这个包中

//写数据
Socket sc = new Socket("127.0.0.1", 5000);
PrintWriter writer = new PrintWriter(sc.getOutputStream());
writer.println("带换行的测试数据");
writer.print("不带换行的测试数据");
writer.close()

//读数据
Socket sc = new Socket("127.0.0.1", 5000);
InputStreamReader stream = new InputStreamReader(sc.getInputStream());
BufferedReader reader = new BufferedReader(stream);
String message = reader.readLine();
reader.close();

//服务器应用程序
//accept()方法会在等待用户的socket连接时闲置,有用户连接时,会返回一个Socket(在不同的端口上)以便和客户端通信。
//Socket与ServerSocket的端口不相同,因此ServerSocket可以空出来等待其他用户。
ServerSocket serverSock = new ServerSocket(5000);
Socket sock = serverSock.accept();

线程:独立的线程,代表独立的执行空间,Thread(java.lang包中,无需import)是表示线程的类,它有启动、连接线程和让线程闲置的方法。

  • 主线程 + 由程序启动的线程;
  • 每个Java应用程序会启动一个主线程,并将main()放到执行空间的最开始处。Java虚拟机只会负责主线程的启动(以及比如垃圾收集所需的系统用线程);
  • 多线程:有多个执行空间,Java也只是在低层操作系统上执行的进程,一旦轮到Java执行时,Java虚拟机会执行执行空间最上面的字节码,在100毫秒内,执行程序代码会被切换到不同空间的不同方法;
  • Thread对象不可以重复使用,一旦线程的run()方法完成之后,该线程就不能再重新启动,而且会走向死亡。Thread对象可能还在堆上,类似其他或者的对象还能接受一些方法的调用,但是已经永久失去线程的执行性,只剩下对象本身;
  • 线程的优先级可以对调度器产生影响,但是没有绝对的保证。优先的级别会告诉调度器某个线程的重要性;
  • 创建线程的方法
    • 将Thread做个子类来覆盖掉run(),但不建议,因为不符合面向对象的设计;
    • 创建实现Runnable接口的独立类;
//实现Runnable接口来建立给thread运行的任务,这个接口只有一个方法:public void run(),因为是接口,所以无论怎么写它都会是public.
public class MyRunnable implements Runnable {
  public void run() {		//只有一个方法需要实现,无参,要执行的内容放在里面,是新线程执行的第一项方法
    String threadName = Thread.currentThread().getName();		//可以获取线程名称
    go();
  }
  public void go() {
    doMore();
  }
  public void doMore() {
    System.out.println("top on the stack.");
  }
}

class ThreadTester {
  public static void main(String[] args) {
    Runnable threadJob = new MyRunnable();
    Thread myThread = new Thread(threadJob);		//线程实例创建
    myThread.setName("Alpha thread");		//线程可以设置名字,后续也能根据名字区分不同线程,通常用来除错
    myThread.start();		//新的线程启动,变成可执行状态,已经布置好执行空间,会把Runnable对象的方法放到新的执行空间中.
    //等待Java虚拟机线程调度机制决定执行哪个线程,用户无法强制将线程从可执行状态移到执行中.
    //一旦线程进入可执行状态,它会在可执行和执行中两种状态来回切换,也可能进入’暂时不可执行‘状态,即阻塞状态。
    System.out.println("back in main.");
  }
}

//确保线程能够有机会执行的最好方式是让它们周期性地睡眠,但不必刻意使用
try {
  Thread.sleep(1000);		//该方法可能会抛出InterruptedException,这个异常是API用来支持线程间通信的机制,所有调用都需要包在try/catch语句块中。
} catch(InterruptedException ex) {
  ex.printStackTrace();
}

synchronized:同步化,即线程加锁,使用该关键字修饰的方法每次只能被单一线程存取。

  • 同步化的目标是保护重要数据,而不是存取数据的方法。每个Java对象都有一个锁,一把钥匙,
    • 对象的锁只会在同步化的方法上起作用。当对象有一个或多个同步化的方法时,线程只有在取得对象锁的钥匙才能进入同步化的方法;
    • 锁不是配在方法上,而是配在对象上。获取钥匙是由Java虚拟机来处理。如果对象有同步化的方法,则线程只有在取得钥匙的情况下才能进入线程,即没有其他线程已经进入的情况下才能进入。如果对象有两个同步化的方法,就表示两个线程无法进入同一个方法,也表示两个线程无法进入不同的方法;
    • 对象就算有多个同步化过的方法,也只有一把锁。一旦某个线程进入该对象的同步化方法,其他线程就无法进入该对象上的任何同步化线程;
    • 每个对象实例有锁,每个被载入的类也有锁。如果有3个Dog对象在堆上,则有4个锁,3个Dog实例的,1个类的;
  • 同步化会造成查询钥匙等性能上的损耗,会让程序因为同步并行的问题(强制线程排队)而慢下来,甚至会导致死锁现象。原则上最好做最少量的同步化,同步化可以不以函数为单位,也可以对一行或数行指令进行同步化。
//方法加锁
private synchronized vod makeWithdrawal(int amount) {}

//局部语句加锁,使用参数所指定的对象的锁来做同步化,有别的方法可以达到同样的效果,但通常会用当前对象this来同步化
public void go() {
  synchronized(this) {}
}
  • 单例模式(Singleton)
class Accum {
  private static Accum a = new Accum();		//创建Accum类的静态实例
  private int counter = 0;
  private Accum() {}		//私有的构造函数,其他人无法创建它的对象
}
  • 对象的等价
//引用相等性:堆上同一个对象的两个引用
if (a ==b) {}			//根据内存位置计算序号hashCode()或比较字节组合

//对象相等性:堆上两个不同对象在意义上是相同的,即两个引用指向同一个对象或两个对象相等
if (a.equals(b) && a.hashCode() == b.hashCode()) {}		//必须覆盖从Object继承的hashCode()和equals()方法

@@@特别注意:equals() > hashCode(),前者成立,后者必须成立,反之未必.
@如果equals()被覆盖,hashCode()也必须被覆盖.
@equals()默认执行==的比较,即引用相等性检查,如果没有被覆盖过,则两个对象永远不会被视为相同的,因为不同对象有不同字节组合。
@a.equals(b)必须与a.hashCode()==b.hashCode()等值.
  • Java中的集合
    • ArrayList:add()添加元素默认放最后,也可以指定位置,但会降低效率;
    • TreeSet:以有序状态保持并可防止重复,插入String后会自动按照字母顺序排在正确的位置;
    • HashMap:可用成对的name/value来保存和取出;
    • LinkedList:针对经常插入或删除中间元素所涉及的高效率集合,实际上不如ArrayList实用;
    • HashSet:防止重复的集合,可快速地寻找相符的元素;
    • LinkedHaskMap:类似HashMap,但可记住元素插入的顺序,也可以设定成依照元素上次存取的先后来排序;
  • java.util.Collections类中有sort()方法,默认参数是List接口,会把List中的String按照字母排序,ArrayList有实现这个接口;
  • 泛型(generic)功能:Java 5.0后新增功能,使用<>符号,提供更好的类型安全性。
    • 类型安全:虽然可以用在其他地方,但几乎所有泛型程序都和集合处理有关,目的是帮助写出有类型安全性的集合。在泛型出现之前,只能处理成Object类型,无法对类型进行检查,有了泛型能够在编译器确保类型安全,不会到执行期才发现;
    • 常见用法
//创建被泛型化类(如ArrayList)的实例
ArrayList<Dog> arr = new ArrayList<Dog> ();
//声明和指定泛型类型的变量
List<Song> songList = new ArrayList<Song> ();
//声明(与调用)取用泛型类型的方法,类似上面这个
void foo(List<Song> list) {}
x.foo(songList);
  • 类型参数,E会被指定的类型参数所取代,该字母是指代元素的习惯用法
//泛型的类,即类的声明用到了类型参数
public class ArrayList<E> extends AbstractList<E> ... {
  public boolean add(E o)		//声明类的类型参数后,可以在该类或接口类型用在任何地方
}

//泛型的方法,即方法的声明特征用到了类型参数,同时Java设计了给参数化类型添加现在的方法,如下面的Animal子类限制
public <T extends Animal> void takeThing(ArrayList<T> list)	{}	//因为前面声明T,所以参数可以用T
//如果类本身没有类型参数,可以在方法返回类型前声明,这里T意味着任何一种Animal

//泛型的方法,第二种语法,使用万用字符,具体使用T还是?需要根据是否要添加元素决定
public void takeThing(ArrayList<? extends Animal> list) {}

//万用字符
public static <T extends Comparable<? super T>> void sort(List<T> list) {}	//<? super T>代表Comparable的类型参数必须是T或T的父型
  • ?是万用字符,使用带有<?>的声明是,编译器不会让你添加任何东西到集合中,即可以操作读取,但是无法新增集合元素,否则会编译报错。
  • 集合排序
//1.类实现Comparable接口
class Song implements Comparable<Song> {
  public int compareTo(Song s) {
    reutrn title.compareTo(s.getTitle()):
  }
}
ArrayList<Song> songList = new ArrayList<Song> ();
Collecions.sort(songList);

//2.自定义Comparator
class ArtistCompare implements Comparator<Song> {
  public int compare(Song one, Song two) {
    return one.getArtist().compareTo(two.getArtist());
  }
}
ArtistCompare artistCompare = new ArtistCompare();
Collections.sort(songList, artistCompare);
  • java.util.Collection:主要有3个主要接口,List、Set和Map,Map没有继承Collection接口,但也是集合框架的一部分。
  • List:顺序,知道索引位置的集合,可以多个元素引用相同的对象;
  • Set:去重,不允许重复的集合,判断对象是否在集合中(比较hashcode),不会有重复元素引用相同对象,即使仅仅相等也不行;
//addAll()可以复制其他集合元素,效果类似逐个添加

//使用HashSet去重:HashSet去重的依据是判断对象相等性,所以Song需要覆盖从Object继承的hashCode()和equals()
HashSet<Song> songSet = new HashSet<Song> ();
songSet.addAll(songList);			

//使用TreeSet去重:会一直保持集合处于有序状态,排序会损耗很小的处理能力,默认无参构造函数使用对象的compareTo()方法排序,也可以传入Comparator;
//TreeSet元素必须是Comparable
TreeSet<Song> songSet = new TreeSet<Song> ();
songSet.addAll(songList);
  • Map:键值对,key不可以重复,通常key是String,但也可以用任何Java的对象(包括autoboxing的primitive);
HashMap<String, Integer> scores = new HashMap<String, Integer> ();
scores.put("Tom", 10);
socres.get("Tom");
System.out.println(scores);		//以key=value的形式打印,{Tom=10}
  • 部署应用程序的选择
    • 本机:JAR;
    • 介于两者之间:Web Start(abbr. JWS)、RMI app;
    • 远程:HTTP;
  • 程序打包进JAR,可执行的JAR代表用户无需把文件抽出来即可运行,程序能够在类文件都保存在JAR的情况下执行,因为使用manifest文件可以告诉Java虚拟机哪个类有main()方法。
cd Project/source
javac -d ../classes *.java		#-d参数指定编译文件输出目录,不使用则默认当前路径
cd ../classes
创建manifest.txt描述哪个类带有main()方法:Main-Class: MyApp			#后面没有.class,而且建议有换行
jar -cvmf manifest.txt app1.jar *.class		#使用jar工具创建带有所有类和manifest的JAR包
java -jar app1.jar			#Java虚拟机能够从JAR中载入类,并调用该类的main()方法
  • 防止类名冲突是包的主要目的之一,但这需要保证包名不会重复,最好的办法是在前面加上反向domain名称。
com.headfirstjava.projects.Chart
//反向domain名称:com.headfirstjavar
//projects:项目名
//Chart:类名第一个字母大写

@必须把类放在和包层次结构相对应的目录结构下。
//在类中加入包指令,程序源文件的第一个语句,每个原始文件都只能有一个包指令,因此同一个文件中的类都会在同一个包中,也包括内部;
package com.headfirstjava;
//只设置包指令加入源文件,类不会真的被加入包,必须类也在相对应的目录结构下,否则编译起来会很麻烦。
  • 把包打包:类使用了包名后,编译器和Java虚拟机都需要能够找到指定的类和所用到的其他类,所用需要指明源文件路径,不能使用”简写”,必须指定要执行main()的类的完整名称。
cd Project/source
javac -d ../classes com/headfirstjava/*.java		#编译需要指明取得源文件的路径,-d参数也会按照包的组织结构输出到classes
java com.headfirstjava.Demo			#执行需要指明完整名称
cd ../classes
创建manifest.txt描述哪个类带有main()方法:Main-Class: com.headfirstjava.Demo
jar -cvmf manifest.txt app2.jar com		#只需要指定com目录就行,其下整个包的类都会被包进去JAR
  • 条列和解压的jar命令
    • 将JAR包内容列出:jar -tf A.jar,tf代表Table File,也就是列出文件的列表;
    • 解压JAR包内容:jar -xf A.jar,xf代表eXtract File,就像unzip一样,结果是在当前目录下生产META-INF和com目录;
      • META-INF:META information,jar工具会自动创建出这个目录和MANIFEST.MF文件,之前创建的manifest.txt不会被带进JAR,但它的内容会放进真正的manifest中;
  • 协助用户安装Java:很多厂商提供工具来创建installer,Installshield、InstallAnywhere和DeployDirector等都提供安装程序工具。
  • JWS:Java Web Start,通过网络发布的应用程序,用户通过网络链接启动JWS,程序一旦下载后,就能独立于浏览器执行,和从网络上下载可执行JAR一样。
    • 类似浏览器的plug-in的小Java程序,即JWS的helper app,主要用来管理下载、更新和启动JWS程序;
    • JWS下载程序JAR包时,会调用程序的main(),然后用户就可以通过JWS helper app启动应用程序而无需回到网页;
    • JWS还可以检测服务器上应用程序的局部更新,无需用户介入,下载和整合更新过的程序;
    • 可以从Sun下载JWS,如果装了JWS而Java版本低,J2SE也会自动下载;
  • 一般来说,对象的方法调用都是在相同的Java虚拟机上面进行的。Java虚拟机只会知道自有堆的引用。
  • RMI:远程过程调用
    • 包含对象:客户端、客户端辅助设施stub、服务器辅助设施skeleton、服务器;
    • 流程:辅助设施是个在实际上执行通信的对象。客户端辅助设施会将调用的信息(如方法名和参数内容)传递给服务端,等待响应;服务器的辅助设施会通过Socket连接来自客户端设施的要求,解析打包送来的信息,然后调用真正的本地服务,即真正的方法逻辑都在服务器。取得返回值后再包装送回(通过Socket的输出串流)给客户端辅助设施。客户端辅助设施会解开信息给客户端对象;
    • Java RMI提供客户端和服务器端辅助设施对象,同时也知道如何提供相同的方法给客户端调用,也提供执行期所需全部的基础设施,包括服务的查询以让客户端能够找到与取得客户端的赋值设施(真正服务的代理人);
    • RMI调用和一般调用不同的一点是,客户端辅助设施会通过网络发出调用,有风险并会抛出异常;
    • 使用RMI必须要决定协议:JPMP或IIOP
      • JRMP:RMI原生协议,为Java对Java间的远程调用而设计;
      • IIOP:为了CORBA(Common Object Request Broker Architecture)而产生,能过调用Java对象或其他类型的远程方法。CORBA通常比RMI麻烦,因为两端不都是Java时会有非常麻烦的转义和交谈操作;
  • Jini使用RMI和别的协议,是增强版的RMI,多了几个关键功能:
    • adaptive discorvery(自适应探索);
    • self-healing networks(自恢复网络);
  • String不变性:创建新的String时,Java虚拟机会把它放到称为”String Pool”的特殊存储区中,如果已经出现同值的String,Java虚拟机不会重复建立String,只会引用已存在者,因为String是不变的,引用变量无法改变其他参考变量引用到的同一个String值。String Pool不受垃圾收集器管理。如果要执行一堆String操作,StringBuilder这个类更合适。
  • 包装类不变性:包装对象没有setter,包装对象创建后就无法改变该对象的值。
  • 断言:方便调错,如果没有特别设定,程序中的assert命令会被Java虚拟机忽略。
//使用断言
assert(height > 0);		//true继续执行,false抛出AssertionError
assert(height > 0) : "后面可跟任何非null的合法Java语句";			//千万不要在assert中改变对象状态,否则开启断言执行时可能会改变程序的行为

//带有断言的编译和执行
javac Test.java			//不需特殊选项
java -ea Test				//使用ea参数开启断言

静态嵌套类:因为是静态的,所以不需要外层实例也能初始化,很像非嵌套的。但因为是外层的一个成员,所以能够存取任何外层的私有成员。

  • 嵌套类:nested,任何被定义在另一个类范围内的类被称为嵌套的类,不管是否匿名、静态、正常或其他类型。只要在另一个类中,技术上它就被认定是嵌套的类;
  • 内部类:inner,非静态的嵌套类通常被称为内部类。所有内部类都是嵌套类,但不是所有嵌套类都是内部类;
public class FooOuter {
  static class BarInner {
    void sayIt() {
      System.out.println("method of a static inner class.");
    }
  }
}

class Test {
  public static void main(String[] args) {
    FooOuter.BarInner foo = new FooOuter.BarInner();
    foo.sayIt();
  }
}
  • 匿名内部类
button.addActionListener(new ActionListener() {			//传递类的定义,会自动创建该类的实例
  public void actionPerformed(ActionEvent ev) {
    System.exit(0);
  }
});
  • String是不变的,StringBuffer/StringBuilder是可变的,从Java 5.0 开始,StringBuilder取代了StringBuffer。
  • 多维数组:在Java中,二维数组只是个数组的数组,三维数组是数组的数组的数组。
int [][] a2d = new int [4][2];
int [][] x = {{2,3,4}, {7,8,9}};
int x = a2d[2][1];		//元素实际上是对2个元素数组的引用变量
  • 枚举
public enum Members { JERRY, BOBBY, PHIL };

Members n = Members.BOBBY;
if (n.equals(Members.JERRY) {}		//使用方式1
if (n == Members.PHIL) {}					//使用方式2
switch (n) {											//使用方式3
  case JERRY: System.out.println("Jerry");
  case BOBBY: System.out.println("Bobby");
  case PHIL: System.out.println("Phil");
}
    
//特殊的enum形式,在enum中加入构造函数、方法、变量和特定常量
public class SpecialEnum {
  enum Names {
    JERRY("a") {
      public String sings() { return "aaa"; }
    },
    BOBBY("b") {
      public String sings() { return "bbb"; }
    },
    PHIL("c");
    private String instrument;
    Names(String instrument) { this.instrument = instrument; }
    public String getInstrument() { return this.instrument; }
    public String sings() { return "oooooo"; }
  }
  
  public static void main(String[] args) {
    for (Names n: Names.values()) {
      System.out.print(n);
      System.out.print(n.getInstrument());
      System.out.println(n.sings());
    }
  }
}
赞(0)
未经允许不得转载:北凉柿子 » 《head first Java》笔记
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址