前言
之前在校的时候简单读了一遍《Java编程思想》
这本书,老实说真的是晦涩难懂,书中大量的代码看的我晕乎乎的,笔者这里对一些知识点做了笔记,如果有不对的请多多指教!
一切都是对象
- 对象存储到什么地方?
- 寄存器——最快的存储区,数量有限。根据需求分配,无法直接控制
- 堆栈——位于通用RAM(随机访问存储器)中。某些数据存储于堆栈中,特别是对象引用。速度仅次于寄存器
- 堆——用于存放所有的Java对象。当执行new操作时,会自动在堆里进行存储分配
- 常量存储——通常直接存放在程序代码内部
- 非RAM存储——数据存活于程序之外,在程序没有运行时也可以存在。两个基本的例子是流对象和持久化
- 基本类型变量直接存储值并置于堆栈中,因此更加高效。
- 当创建一个数组对象时,实际上就是创建了一个引用数组,并且每个引用都被初始化位
null
。- 当变量作为类的成员使用时,Java才确保给定其默认值。然而此方法并不适用于局部变量,定义于方法内的变量,局部变量不会被初始化
5.
main
方法中的args
用来存储命令行参数。
操作符
- 如果对对象使用
a = b
,那么a和b都指向原本只有b指向的那个对象。==
和!=
比较的是对象的引用。- 基本类型比较内容是否相等直接使用
==
和!=
即可。- Object类的
equals
方法默认使用"=="
比较。- 大多数Java类库都重写了
equals
方法,以便用来比较对象的内容,而非比较对象的引用。- 如果对
char、byte
或short
类型数值移位处理,在进行移位之前会先被转换为int
类型,并且得到的结果也是一个int类型的值。
初始化和清理
- 在Java中,"
初始化
"和"创建
"被捆绑在一起,两者不能分离。- 涉及基本类型的重载:
**如果传入的数据类型(实参)小于方法中声明的形参,实际数据类型就会被提升。
char型有所不同,如果无法找到恰好接受char类型参数的方法,就会把char直接提升至int。
如果传入的实际参数较大,就会强制类型转换**。- 如果已经定义了一个构造器,编译器就不会帮你自动创建默认构造器。
this
关键字只能在方法内部调用,表示对"调用方法的那个对象
"的引用。- 在构造器中可通过
this
调用另一个构造器,但却不能调用两个构造器。此外,必须将构造器调用置于最起始处,否则会报错。- 在
static
方法的内部不能调用非静态方法,反过来是可以的。- 垃圾回收器只知道回收那些经由new分配的内存。
native
(本地方法)是一种在Java中调用非Java代码的方式。- 在类的内部,变量定义的先后顺序决定了初始化的顺序。
即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。- 无论创建多少个对象,静态数据都只占一份存储区域。
- 静态初始化只有在必要时刻进行。
只有在"第一个对象被创建"或者"第一次访问静态数据"的时候,它们才会被初始化。此后静态对象不会再被初始化。- 初始化的顺序是先静态对象(如果它们还没被初始化过),而后是非静态对象。
复用类
- 每一个非基本的对象都有一个
toString
方法。- Java会自动在子类的构造器中插入对父类构造器的调用。所有构造器都会显示或隐式地调用父类构造器。
- 构建过程是从父类"
向外
"扩散的,所以父类在子类构造器可以访问它之前就已经完成了初始化。- 调用父类构造器是你在子类构造器中要做的第一件事。
- 向上转型:可以理解为"
子类是父类的一种类型
"。- 对于基本类型,
final
使数值恒定不变。对于对象引用,final
使引用恒定不变,即无法再把它指向另一个对象。final
类:防止别人继承。
final
参数:你可以读参数,但却无法修改参数。(一般用来向匿名内部类传递数据)
final
方法:把方法锁定,以防任何继承类修改它的含义。
多态
- 把对某个对象的引用视为对其父类型的引用的做法被称为向上转型。
- 只有非
private
方法才能被覆盖。- 域和静态方法不是多态的。当子类转型为父类引用时,任何域访问操作都将由编译器解析,所以不是多态的。
- 调用构造器要遵循下面的顺序:
- 调用父类构造器
- 按声明顺序调用成员的初始化方法
- 调用子类构造器
- 为什么在子类构造器里要先调用父类构造器???
答:在构造器内部,我们必须要确保要使用的成员都已经构建完毕。为确保这一目的,唯一的方法就是首先调用父类构造器。那么在进入子类构造器时,在父类中可供我们访问的成员都已得到初始化。
- 继承与清理
(1)当覆盖父类的
dispose()
方法时,务必记住调用父类版本的dispose()
方法,否则父类的清理动作就不会发生。
(2)应该首先对子类进行清理,然后才是父类。这是因为子类的清理可能会调用父类的某些方法,因此不该过早地销毁它们。
- 构造器内部的多态行为
Class A{ void draw(){print("A.draw()");} A(){draw();} } Class B extends A{ void draw(){print("B.draw()");} } Class Test{ public static void main(String[] args){ new B(); } }
输出:B.draw()
在B构造器中调用父类A构造器,在里面调用的是子类B覆盖后的draw()方法。
因此,编写构造器时有一条有效的准则:"
如果可以的话,避免在构造器内部调用其它方法
"。
接口
- 包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。
- 如果继承抽象类,那么必须为基类中所有的抽象方法提供方法定义。如果不这样做,那么子类就必须限定为抽象类。
- 接口中的域隐式地是
static
和final
的。- 接口中的方法默认是
public
的,因此实现接口中的方法时必须定义为public
的。否则其访问权限就降低了,这是Java不允许的。- 通过继承来扩展接口
interface A{....} interface B{....} interface C extends A,B{.....}//仅适用于接口继承
一般情况下,只可以将
extends
用于单一类,但是可以引用多个基类接口。
- 嵌套在类中的接口可以是
private
的。
嵌套在另一个接口中的接口自动是public
,而不能声明为private
的。- 当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,
private
接口不能在定义它的类之外被实现。
内部类
- 当生成一个内部类的对象时,内部类对象隐式地保存了一个引用,指向创建它的外部类对象。(静态内部类除外)
内部类对象能够访问外部类对象的所有成员和方法(包括私有的)。.new
和.this
class A{ class B{} }
(1)创建内部类对象:
A a=new A(); A.B b=a.new B();
(2)生成外部类对象引用:
class A{ class B{ public A getA(){ return A.this; } } }
- 如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是
final
的。- 当创建静态内部类的对象时:
(1)不需要其外部类对象
(2)不能从静态内部类对象中访问非静态的外部类对象- 不管一个内部类被嵌套多少层,它都能够访问所有它嵌入的外部类的所有成员。
- 类文件命名规则:
外部类名字+$+内部类名字
(1)普通内部类:A$B
(2)匿名内部类(编译器会生成一个数字作为其标识符) A$1
类型信息
- 如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),
toString()
方法会被自动调用,以生成该对象的String。- 每当编写并且编译了一个新类,就会产生一个Class对象。更恰当地说,是被保存在一个同名的.class文件中。
- **Java程序在它开始运行之前并非完全被加载,其各个部分是在必需时才加载的。
类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件**。Class
对象仅在需要的时候才被加载,static
初始化块是在类加载时进行的。Class.forName("A")
用来加载类A并获取A.Class对象。字符串必须使用全限定名,包括包名。Class AClass=A.class
当使用.class来创建Class对象时,不会自动地初始化该Class对象。- 如果一个
static final
值是编译期常量,就像a那样,那么这个值不需要对初始化类就可以被读取。读取b和c则需要先对类进行初始化。static final int a=1; static final int b=ClassInitialization.rand.nextInt(1000); static int c=1;
泛型
- 擦除
- 可以声明
ArryaList.class
,但不能声明ArrayList<Integer>.class
;Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2);//返回true
List<String>
和List<Integer>
在运行时事实上是相同的类型,两者都被擦除为List
类型。
- 擦除的补偿
public class czy<T>{ public static void f(Object arg){ (1)if (arg instanceof T){} //错误 (2)T t = new T(); //错误 (3)T[] array = new T[10]; //错误 } }
(1)无法使用
instanceof
是因为其类型信息已经被擦除了。
(2)new T()
失败,部分原因是因为擦除,另一部分原因是因为编译器不能验证T具有默认构造器。
除非引入类型标签public boolean f(Class<T> kind,Object arg){ t = kind.newInstance(); //正确 return kind.isInstance(arg); //正确 }
<? extends T>
表示类型的上界,表示参数化类型可能是T或T的子类public void f(List<? super Apple> apples){ apples.add(new Apple()); //正确 apples.add(new GreenApple()); //正确 GreenApple继承自Apple apples.add(new Fruit()); //错误 }
向apples其中添加Apple或Apple的子类型是安全的。
因为Apple或者GreenApple肯定是<? super Apple>的子类,所以编译通过。
- List<?>看起来等价于
List<Object>
,而List实际上也是List<Object>
。- 自动包装机制不能应用于数组。
- 由于擦除的原因,重载方法会产生相同的类型签名。
public class Czy<W,T>{ void f(List<W> v){} void f(List<T> v){}//错误 }
容器深入研究
- **
Arrays.asList()
会生成一个固定尺寸的List,该List只支持那些不会改变数组大小的操作。
任何对底层数据结构的尺寸进行修改都会抛出一个异常**。应该把Arrays.asList()的结果作为构造器参数传递给Collection,这样就可以生成允许使用所有方法的普通容器。
比如:List<String> list = new ArrayList<>(Arrays.asList(a));
Collections.unmodifiableList()
产生不可修改的容器。- 散列码是"
相对唯一
"的,用以代表对象的int
值。- 如果不为你的键覆盖
hashcode()
和equals()
,那么使用散列的数据结构(HashSet,HashMap,LinkedHashSet,LinkedHashMap)就无法正确处理你的键。LinkedHashMap
在插入时比HashMap
慢一点,因为它维护散列表数据结构的同时还要维护链表(以保持插入顺序)。正是由于这个链表,使得其迭代速度更快。- HashMap使用的默认负载因子是
0.75
.- Java容器采用快速报错(
fail-fast
)机制。
它会检查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其它进程修改了容器,就会抛出异常。 防止在你迭代容器的时候,其它线程在修改容器的值。WeakHashMap
允许垃圾回收器自动清理键和值。Iterable
接口被foreach
用来在序列中移动。如果你创建了任何实现Iterable
接口的类,都可以将它用于foreach
语句中。foreach语句可以用于数组或其它任何Iterable,但这并不意味着数组也是一个Iterable。
- 总结
(1)如果要进行大量的随机访问,就使用ArrayList;如果要经常从表中间插入或删除元素,就应该使用LinkedList。
(2)各种Queue以及栈的行为,由LinkedList
提供支持。
(3)HashMap
用来快速访问。
TreeMap
使得“键”始终处于排序状态,所以没有HashMap
快。
LinkedHashMap
保持元素插入的顺序。
(4)Set
不接受重复元素。
HashSet
提供最快的查询速度。
TreeSet
保持元素处于排序状态。
LinkedHashSet
以插入顺序保存元素。
(5)新程序中不应该使用过时的Vector、Hashtable和Stack
。
总结
《Java编程思想》这本书笔者目前读到容器这部分,现在觉得这本书不太适合新手入门,新手我更推荐去看《Java核心技术》,等以后腾出时间了再把后面的补上。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。