《Java核心技术:卷1—基础篇》二次阅读笔记

第一日

  1. strictfp对函数约定使用精准浮点运算,要求运算过程中都截取,已保证在不同机器上结果一致
  2. 类型转换规则与c++不同,详情见反思录
  3. subString之类的函数实际上返回的都是新建的String(其实想想也知道,毕竟String不可修改)
  4. Scanner接受的文件可能不存在,PrintWriter接受的文件或文件名可能也不存在,java将报告一个异常
  5. Scanner直接接受一个字符串作为参数,将会将字符串解析为输入的内容,而不是文件名
  6. breakcontinue都有带标签版本
  7. 如果启动java程序的环境支持控制台,则可以使用System.console
  8. java的启动参数没有文件名,args就是第一个参数

第二日

  1. PrintWriter.printf中对场宽的限定,中文字符实际长宽2,而Java认为是1
  2. final应用在可变的类上,将导致混乱。不可变的类是指没有任何一个方法能改变类数据的类,如String
  3. System.outpublic static final的,但是System.setOut可以修改out是因为它是native方法,它能绕开java的存取控制机制。
  4. c++中的static有3种语义(前两种沿用c):多次函数调用保持值不变、关闭变量的外部链接、隶属于类的成员/方法属性,而Java只取了第三种语义。
  5. c++因为宽松的隐式转换导致容易出现重载多个参数时的二义性,而Java相对好很多。详情见反思录
  6. c允许在全局作用域上多次定义未初始化的全局变量,而c++不允许
  7. c++初始化分为静态初始化和动态初始化,动态初始化发生在静态初始化之后。
  8. c++对构造函数初始化列表中没有提及的内置类型成员的初始值依赖于对象的作用域(在局部作用域中这些变量不被初始化,而在全局作用域中他们被初始化成0),而Java对类成员始终初始化。
  9. Java对类成员的显式域初始化的过程是严格顺序执行的,即使用方法初始化(动态初始化)也不会被推迟(这里可能会使用还未初始化的变量,毕竟编译方法时类所有变量都已经可见)。
  10. Java调用构造器的具体处理步骤:
    (1)将所有数据域初始化为默认值
    (2)按照类声明中出现的次序,依次执行域初始语句或初始化块(注意执行域初始化语句时和初始化块时,所有变量都是可见的)
    (3)如果构造器第一行调用了其他构造器,则执行其他构造器
    (4)执行构造器主体
  11. Java的finalize方法在对象被垃圾回收机销毁时调用,但是调用实际无法被预测,紧缺的资源最后还是手动释放。
  12. 当一次导入包中所有类时,若导入的两个包存在同一个类时,当这个类被使用时将会出现二义性,而得到编译错误,解决办法就是使用import指明导入的是哪个类。
  13. 没有被导入的类,也可以直接使用包名使用。
  14. import static静态导入,使用静态导入则可以不使用类名访问其他类的静态成员。
  15. C++更改父类部分成员的访问权限可以在要设置的访问权限下使用using
  16. 如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本,毕竟在构造或析构时,对象可能不完整。
  17. C++如果采用非public继承将导致无法将子类绑定在基类的指针或引用上,即转型失败,因为非public子类对基类根本没有实际的访问权限,子类对父类的部分成员更改并不影响绑定特性。
  18. C++和Java重写方法时,都可以将返回值是父类引用(指针)换成子类引用(指针)。
  19. Java中static(不存在对象) final(显示禁止继承) private(没有办法使用基类的引用访问这方法执行动态绑定,所以用静态绑定可以提高性能,注意访问权限只能是越来越宽松,因此不能基类是public而子类是private的情形)属性的方法和构造器(对象不完整)都是静态绑定的。
  20. C++98中能使用虚基类的技巧达到禁止继承,而C++11和Java支持使用final禁止类继承,同时final还能阻止重写函数。

第三日

  1. Java的instanceof能识别实例的真正类型,判断能否转换到继承树上的指定类型,在进行基类向子类转换前最好先检查一下。
  2. 包含一个或多个抽象方法的类本身必须被声明为抽象的。
  3. 重写Object.equals最好重写hashCode
  4. C++ vector<T>(int initSize), Java ArrayList<T>(int initialCapacity)
  5. 自动打包、拆包是编译器认可的,而不是虚拟机。编译器在生成字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
  6. 自动打包规范要求boolean、byte、小于等于127的char、介于-128和127之间的shortintlong打包到相同的对象中。
  7. 自动打包不支持类型转换,即Long a = 100; 这样的语句是错误的。
  8. 包装类的parseXXX静态方法返回的是基础类型,而valueOf静态方法返回的是包装类型。
  9. 因为包装器是不可变的类型,所以也没有办法修改存储的值,从而不能在值传递中穿透性的修改值,而持有者类型xxxHolder因为有set方法,所以可以在值传递的过程中,穿透性的修改值。
  10. 可变参数表(只能作为最后的参数)实际上会转换为某个类型的数组,int... args只能接受多个int或一个int[],如果要接受任意类型就要使用Object,这样接受多个Object(基本类型被自动打包)或一个Object[],注意这里int[]只能转换为Object而不能是Object[],因此传递一个Object[]实际是传递了多个Object,而传递一个int[]实际只传递了一个Object(其他基本类型同理)。
  11. C++的枚举只是一些名字的集合,这些集合的元素和整型有着对应关系,而Java中枚举类是一个真正的类和该类的若干个实例,甚至可以有构造器、成员和域。并且所有的枚举类都由Enum派生而来,Enum提供了valueOf静态方法根据类的元信息和字符串可以找到对应的实例,提供了toString可以让实例找到枚举常量名,还提供了ordinal方法让实例找到自己的位置序号。
  12. 未检查异常(运行时异常)可不捕获,已检查异常则必须捕获并提供处理,Java所有异常都从这两类异常派生而来。
  13. Java启动时要加载所需要的类,这显得非常慢,一个可以让用户看起来快的幻觉,main方法中不要显示的引用其他类,而先显示一个启动界面,然后利用Class.forName手动加载其他类。
  14. Class对象实际表示的是一个类型,而这个类型未必是一种类,比如基础类型int也有对应的Class
  15. 反射机制能获取不可访问域的信息,但是不能获取它的值、调用函数等。如果没有受到安全管理器的阻止,可以用setAccessible覆盖访问控制。
  16. Arrays.copyOf实际上也利用了反射机制?为什么不能用System.arraycopy拷贝数据,然后用泛型确定类型。
  17. 接口中的抽象方法只能是public,默认是public,可以用public描述,但是不能用其他权限描述。同时public是最高权限,其他类实现接口时,不能分配更低的权限,因此也只能是public,从而保证了接口的有效性。同理常量只能是public static final
  18. 基类决定子类equalscompare的概念,最好将这个方法设为final阻止子类修改其概念。子类自己决定equalscompare的概念则应该使用getClass确定类型一致,而不是直接强制转换(对称性无法保证)。
  19. Object类中,clone方法被声明为protected,因此无法直接在外部使用,但是它充当一个默认实现,因为所有类都是Object的子类,所有子类都可以访问protected,但是子类要实现Cloneable并重新定义clone为public(无论默认实现能不能满足要求,都必须这么干,只是重新定义clone时实现不同,默认实现能满足要求时,其实现就是直接调用super.clone),这才算完成了java中的克隆机制。
  20. 数组默认实现的clonepublic的。
  21. clone也是一个Native方法。毕竟在基类的方法中创建子类对象,访问子类的作用域并拷贝是比较困难的(或许反射可以做到,但是反射弊端还是很多的)。
  22. clone会抛出已检查异常。
  23. 将内部类声明为private可以阻止其他类创建内部类的实例,只有内部类能使用private访问权限。
  24. C++嵌套类,在较新的标准中内部类是和外部类成员具有相同访问权限的,Java的内部类也与外部类成员具有相同的访问权限。
  25. 内部类也是编译器实现,虚拟机不可见的。编译器将内部类编译成单独的一个类,给构造方法加上到外部类的引用,在外部类中添加静态方法根据实例返回实例的成员变量/或修改变量,从而给内部类提供特殊的访问权限(内部类中访问外部类变量会被翻译为特殊静态方法的调用)。特殊的静态方法中含有特殊的符号,从而无法被程序员使用,但精心修改类文件可以调用这个方法(这个需要hack能力了)。同时内部类的私有权限实际上也是编译器动的手脚,虚拟机一无所知(实现没看明白。。)。
  26. final的变量(包括基础变量)只可初始化一次,之后不能改变。这个初始化,可以在构造器中用赋值号初始化,也可以在类域中初始化,而不一定要在声明时初始化(即只要在对象完整构造前初始化一次,这点和C++是一样的,但是C++执行构造函数体时,对象已完整构造,只能初始化列表中初始化,并且在C++11之前非staticconst变量不能就地初始化)。
  27. 匿名内部类因为没有名字,所以也没有构造器。取而代之的是将构造器参数传递给基类的构造器(若继承的是接口,则不能有任何构造器参数)。
  28. 代理可以在运行时创建全新的类(暂时跳过)。
  29. 跳过GUI、applet

第四日

  1. C/C++和Java的十进制科学计数法采用e/E尾数指数均为十进制底数10,十六进制科学计数法采用p/P尾数为十六进制指数为十进制底数2。(注意十六进制中e被解释为一个数位)
  2. C的const是定义只读变量,C++才把const当做常量,这使C在需要一个常量表达式的时候const变量不能出现。同时C中const type*类型向type*转换,const会被抛弃,C++则禁止这种转换,如果实在要去除const属性,可以使用(type*)(void*)或者const_cast
  3. “如果出现RuntimeException异常,那么就一定是你的问题”是一条相当有道理的规则。应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException;应该通过在使用变量前检测是否为空来杜绝NullPointerException。同理,其他RuntimeException也通常是可以修改程序来避免的。
  4. Java语言规范将ErrorRuntimeException称为未检查异常,所有的其他异常称为已检查异常。已检查异常需要提供异常处理器。
  5. RuntimeException这个名字很容易让人混淆,实际上所有异常都发生在运行时。
  6. C++有两个异常处理类,一个是runtime_error,一个是logic_errorlogic_error与Java中RuntimeException一样表示程序本身的问题,应该通过修改程序避免,而runtime_error与Java中其他异常一样,表示不可预测运行时问题。
  7. 在进行移位运算时,当向左边移动时,如1 << 35, 对于int类型,由于其占有4个bytes(32bits),因此在Java中,大于32的移位将对32取模,即1 << 35的结果等于1 << 3,以此类推,long将会对64取模。对于int类型而言,如果确实需要获取32位以上的移位,需要将返回值的类型提升到long即可。
  8. Java中支持0长度的数组定义,如int et = new int[0];在C/C++中,该写法将会导致编译错误。(GNU支持零长数组)
  9. switchcase语句中引用枚举常量时不需要再加上枚举的类型名。
  10. 为了规避局部类只能访问final局部变量的限制,既一次赋值之后不能再被重新赋值。但是我们可以通过数组的方式进行巧妙的规避,在下例中数组counter对象本身是final的,因此他不可以被重新赋值,然而其引用的数组元素则可以被重新赋值。
  11. C++没有给出异常声明,函数可能抛出任何异常,Java没有给出异常声明将不能抛出任何已检查异常。
  12. C++和Java在重写一个方法时抛出的异常不能比基类中抛出的异常多。
  13. C++可以throw任何类型的值,而Java只能抛出Throwable的子类。
  14. C++中宏assert(), Java中assert 条件[:表达式]
    *15. 日志跳过

第五日

  1. C++泛型方法将类型参数放在函数名后,可能会引入二义性,如g(f<a,b>(c))。Java则将泛型方法的类型参数放在了函数名前。
  2. 使用泛型时,即便是本类的方法,如果要强制指定类型参数,也必须要带上类型名。Main.<Integer>getFirst(new Integer[]{1,2,3})
  3. Java和C++中int a[]={1,2,3}中只是一种初始化方式,不要认为{1,2,3}就是一个数组。(其他动态类型的语言,通常更自由,直接认为{}就是创建数组或者列表)
  4. Java泛型对原型的处理是将原型的泛型参数擦除,用限定类型替换,如果没有指定限定类型则使用Object替换。
  5. 如果对泛型的类型参数做限定,子类必须在最前,接口在后(为了提高效率,避免强制转换,最好将标签接口放在最后)。
  6. 虚拟机中没有泛型,只有普通类和方法。所有类型参数都被限定类型或者Object替换。桥方法被合成来保持多态。编译器严格检查限定类型(虽然虚拟机不知道),为了保持类型安全性,必要时插入强制类型装好。
  7. 不能用基本类型实例化类型参数,运行时类型查询只是适用于原始类型(类型参数已经被擦除)。
  8. 泛型类不能扩展Throwable
  9. 参数化类型的数组不合法。
  10. 不能直接实例化类型参数变量(同时也不能得到Class信息),毕竟类型参数都被擦除了。如果实在需要实例化,应该通过方法参数从外部传入Class
  11. 泛型类的静态上下文中类型变量无效。
  12. Java语言中,你只能根据签名(函数名和参数)来区分函数,而虚拟机则可以根据函数的全部信息来区分函数(包括返回值)。
  13. 子类继承泛型类的某个版本,需要特别注意,详情见反思录。
  14. 可以将子类的数组绑定到父类的数组引用上,如果试图将一个父类的对象绑定到数组的某个位置,虚拟机将会抛出ArrayStoryExpection。C++不存在这样的问题,因为C++的数组实际上是指向首元素的指针,如果让父类的指针指向了子类数组的首地址,那么数组中全都是对象实例,实例赋值会自动裁剪对象,所以子类给父类赋值是合法的;如果让指向父类的动态数组(指向指针的指针)A*(*),试图完全模拟Java的情况,会发现C++的指针无法从B*(*)转到A*(*),虽然C++允许将子类的指针隐式转换到父类的指针,但是B*A*并不存在什么关系。如果使用强制转换,显然这就已经属于未定义行为了。

第六日

  1. C/C++数组,结构体,类都是作为一个整体(内部总是从低地址向高地址),而和其他变量在一起时,则是根据是堆和栈而定。
  2. 通配符运用为<? extends Base>将使得泛型类中的方法无法接受Base类型的参数,而通配符运用为<? super Base>将使得泛型类中的方法无法返回Base类型的参数。
  3. 要想支持擦除的转换,就需要强行一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同类型参数化。
  4. C++的类模板的成员函数仅在被调用时实例化,所以一个类可能因为不满足某些条件,只能使用模板里的部分方法,如std::vector去实例化成一个没有默认构造函数的类。
  5. C语言中函数声明中参数的名字没有实际意义,而参数列表起到约束调用参数作用。C方式的符号文件只有名字是有用的。甚至C语言早期存在省略参数列表的写法(只留下一对圆括号),这种写法保留至今(兼容,但不推荐)。而C++不允许,毕竟要根据参数进行重载。
  6. C语言调用函数前,可以不声明函数原型(不推荐,返回值默认intvoidint以下的整型也可用),而C++禁止这种做法。

第七日

  1. C/C++内置sort是快速衍生排序(实际上是内省排序),C++为list设置特有的sort执行归并排序,而Java集合框架内置的sort是归并衍生排序。
  2. Java线程分为new,runable,blocked,waiting,timed waiting, terminated六种状态。
  3. Java中的每一个对象都有一个内部锁,如果一个方法用synchronized声明,那么对象的锁将保护整个方法。内部对象锁只有一个相关条件。同时Object类有几个final方法(waitnotifyAllnotify)可以控制这个条件变量。
  4. 将静态方法声明为synchronized也是合法的。如果调用这种方法,该类的class对象将被锁住。因此,没有其他线程可以调用这个类的同步静态方法。
  5. 内部锁和条件存在一些局限。不能中断一个正在试图获得锁的线程,试图获得锁时不能设定超时,每个锁仅有一个条件变量。
  6. 客户端锁定是非常脆弱的,毕竟它不一定使用内部锁。
  7. C++的锁机制不能保证编译器的单线程优化会产生什么结果,volatile可以保证编译器不会过度优化。Java的锁机制可以保证编译器的单线程优化不会影响多线程的语义,但是volatile也是保证编译器不会过度优化,Java在使用锁机制时,volatile并没有什么作用,因为锁已经保证了不会过度优化。

第八日

  1. InputStreamWriteStream底层的read方法和write方法都是阻塞的(在输入数据可用、检测到流末尾或者抛出异常前,方法一直阻塞)。
  2. java(JDK1.6)switch语句支持的类型:bytecharshortintEnum(小于int的整型实际上是发生了整型提升)。JDK1.7时,又增加了String