on-java-8 知识总结(高频部分)

有的内容是高频内容(比如重载、多态),有的内容虽然也很有用但问不到(比如函数式编程、流)。本文已按频率顺序进行调整。

什么是对象

对象有状态、行为、身份。

  • 对象有内部数据(对应状态),
  • 方法(对应行为),
  • 独有的内存地址(对应身份)
    (当然,也要考虑有多台机器,或者持久化到硬盘上的情况)
    对象可以接受request,并做出反应。struct是死的,而对象具有应激性,对象是活的,对象是服务提供者,对象协助我们解决问题。
    如果把对象当成服务提供者,更容易写出高内聚的代码,因为想塞很多东西进一个类的时候你会意识到,这对一个服务提供者来说是不是太多了。此外,这种视角也有利于复用,当需要某种服务的时候,调用相应的服务提供者即可。

对象带来的好处

作为新的数据类型,能更好满足具体需求;
作为服务提供者,抽象程度更高。

封装的好处

避免外部意外修改private成员,减少bug。与const的好处类似。可以让系统的运作符合类设计者的想法,而不至于在实际使用中脱轨。“正确使用”。
使用者不需要掌握太多知识就能使用,“易用”。
开发者能在不影响使用者使用的情况下对接口的实现进行更新。“无感更新”。

代码复用

复用有两种基本方法,聚合和继承。委任不被java直接支持,但IDE一般会帮忙。

委任

委任是聚合与继承的折中。书中的例子是SpaceShip和SpaceShipControls的关系。
控制器应该是SpaceShip的一部分(聚合),同时SpaceShip应当具有SpaceShipControls的接口(类似继承)。

如何选择

不满足is a的时候,就不适合用继承。不应该滥用继承,用之前想一下聚合是否更加合适。

继承动作说明

子类继承父类的成员和方法,
子类和父类应当满足is a的关系(这也体现了里氏代换原则)。
继承之后,一般会添加新方法,或是覆写旧方法。子类是父类的超集。
一个类不仅是它自己,它还是它父类。

创建一个派生类的对象时,如果父类有无参构造函数,则这个无参构造函数会自动且先调用。
如果父类没有无参构造函数,或者你希望调用一个父类的有参构造函数,你需要显式super(参数)。
显式调用时,记得放在一开始,否则会报错。

多态示例

void doSomething(Shape shape) {
shape.erase();
// ...
shape.draw();
}
Circle circle = new Circle();
Triangle triangle = new Triangle();
Line line = new Line();
doSomething(circle);
doSomething(triangle);
doSomething(line);

由于向上造型,所以不会报错;
同样是doSomething,因为实际执行时类的不同而呈现不同的行为。

单根Hierarchy

java中所有类都是Object的子类,而C++中不存在这样一个单根。
C++这样做是为了兼容C,无单根更加自由。
单根结构更容易实现垃圾回收机制。
下面这段没看明白,说的也是单根结构的好处:
And since information about the type of an object is guaranteed to be in all objects, you’ll never end up with an object whose type you cannot determine. This is especially important with system-level
operations, such as exception handling (a language mechanism for reporting errors), and to allow greater flexibility in programming.

array和collections

用于各种对象数量甚至类型不确定的情况。
Collection是一个类(也有一个Collections类,collections.addAll可以输入变长参数),是众多容器的父类,有add/addAll方法(后者将一个collection内所有元素并入)、可以用for-in或迭代器遍历。
各种collection可以通过toArray转换为array。

array和collections是并列关系。
array是定长数组,调用add/delete等影响size的方法会RE。
An array associates numerical indexes to objects. It holds objects of a known type so you don’t have to cast the result when you’re looking up an object. It can be multidimensional, and it can hold primitives. Although you can create arrays at run-time, the size of an array cannot be changed once you create it.

arraylist是java中的vector,linkedlist是java中的链表。LinkedList adds methods to use it as a stack, a Queue or a double-ended queue (deque).

<E>表示泛型,可以解决collections的类型安全问题。泛型的泛,我认为指的是此处的E可以任意。
<?>跟不加没有区别,但这是一种对编译器的宣告,“我了解泛型和类型安全,但在这里我就是要所有类型”。
另外的写法是,<? super boundName>和<? extends boundName>。

下面代码中等式左边用了List,这是为了日后如果想改变,只用改这一行的右边。不过在使用右侧具体类的方法时,如果不是list中有的方法,会报错。

List<Apple> apples = new LinkedList<>();

下面的代码中,第二行显式声明了参数类型。

List<Snow> snow1 = Arrays.asList(new Crusty(), new Slush(), new Powder());
List<Snow> snow4 = Arrays.<Snow>asList(new Light(), new Heavy(), new Slush());

set的实现类包括HashSet、TreeSet、LinkedHashSet(保留插入顺序)。map类似。

list的remove、indexof、contains都依赖于其equals方法。retainAll和removeAll是集合运算中的交和减。set可以修改指定位置的值。list的addAll可以中插。

list、map会自动resize。

作者建议不要在新代码中使用stack,vector和hashtable。
作者主张使用arrayDeque而不要使用stack。原因是java1.0的stack实现很差,但一直因兼容而未废弃。
下面的代码利用泛型,完全使用arrayDeque实现了stack。从中也可见arrayDeque是跟stack很相似的。

import java.util.Deque;
import java.util.ArrayDeque;
public class Stack<T> {
	private Deque<T> storage = new ArrayDeque<>();
	public void push(T v) { storage.push(v); }
	public T peek() { return storage.peek(); }
	public T pop() { return storage.pop(); }
	public boolean isEmpty() { return storage.isEmpty(); }
	@Override
	public String toString() { return storage.toString(); }
}

set最常用于查找,所以一般用hashset,这样查找最快。

collections中不能放基本类型。不过可以寄希望于自动装箱。

队列除了模拟真实的排队,主要用途之一是多进程编程的进程通信。

P844有一个非常全面的println,每个容器类实现了哪些接口。

peek/element, poll/remove

Both peek() and element() return the head of the queue without removing it, but peek() returns null if the queue is empty and element() throws NoSuchElementException. Both poll() and remove()
remove and return the head of the queue, but poll() returns null if the queue is empty, while remove() throws NoSuchElementException.

iterator

iterator主要解决这样的场景:一开始用list实现,但后来想用set实现。
iterator不关心到底是哪一个collection,它只关心collection里面是什么类型。我认为这比C++中的iterator有更好的扩展性(C++中需要指定命名空间,相当于需要指定collection类型)。
Java中的iterator只能单向移动。ListIterator继承自Iterator,可以向前遍历。
只有四个动作,一是iterator(向collection请求一个iterator),二是next(调用后,it指向下一个,然后返回所指内容),三是hasNext,四是remove。

iterator遍历:

Iterator<Pet> it = pets.iterator();
while(it.hasNext()) {
	Pet p = it.next();
	System.out.print(p.id() + ":" + p + " ");
}

看样子,刚获得的iterator会指向一个head,而不是第一个数据。
iterator删除:

it = pets.iterator();
for(int i = 0; i < 6; i++) {
	it.next();
	it.remove();
}

看样子,Java的iterator remove不会造成迭代器失效的问题。其remove都是隐藏的效果。

Producing an Iterator is the least-coupled way of connecting a
sequence to a method that consumes that sequence, and puts far fewer
constraints on the sequence class than does implementing
Collection.
在实现自己的容器时,作者建议实现iterator,而不是实现collection。

只要实现了iterable,就能使用for-in遍历

c++和java在程序员控制权上的取舍

运行前就确定存储位置和生命周期,肯定比运行时才确定要更快。
C++把更多的决定权(比如对象destroy时机)交给了程序员。
而java中,所有的对象都存储在堆上。除了基本类型,一概使用动态内存分配。
java gc只负责释放内存,而这不是清理一个对象的全部。

java异常处理的优势

此处的对比对象是c式的返回值为-1、错误标志位设置等异常处理方法。

  • 被抛出的异常不会被忽略
  • 由于执行路径不同,java异常处理不会干扰工作代码(从可读性和易写性为程序员考虑)
  • 有恢复执行的机会,而不是单纯exit

对象与引用

java中的标识符(变量名)都是引用,就好像一个电视遥控器,而内存中的对象就像电视。
引用,也就是遥控器,存储在栈上;而对象本身,也就是电视,存储在堆里。

变量类型和存储位置

https://www.cnblogs.com/protected/p/6419217.html java中字符串的存储
①寄存器
②堆
③栈
④常量存储
Constant values are often placed directly in the program code, which is safe since they can never change. Sometimes constants are cordoned off by themselves so they can be optionally placed in read-only memory (ROM), in embedded systems.
⑤non-ram存储
serialized object 用于存储传输
persistent object 程序结束后仍能维持状态

需要注意的是,基本类型不是对象,直接存在栈上。
java中各基本类型的大小不随机器变化(所以Java中没有sizeof,所以没有必要),而且没有unsigned类型。

BigInteger和BigDecimal类没有对应的基本类型。后者是定点小数。

java中char是2个字节。

初始化

java中的数组不会出现未初始化和越界访问。如果是对象数组,自动初始化为null。如果是基本类型数组,内存被置0。
至于对象内的成员,如果是基本类型,会有自己的初始值(一般为0,boolean为false),对象内的对象默认为null。
对于C++,则取决于对象的存储位置,栈和堆上的对象内的基本类型是不会自动初始化的。
使用未初始化的局部变量时,java会报错。而C++可能只是发出警告。

可以通过调用new或者接收函数(不能使用尚未初始化的成员作为入参)返回值来初始化对象的成员。
所有成员的初始化都在构造函数执行之前。

静态成员的初始化只在必要时进行(如果该类没有对象或者用户没有用类名进行访问,那就不会创建);
静态成员的初始化早于非静态成员的初始化;
静态成员只会初始化一次(不管有多少个对象)。
可以使用static代码块来对static成员进行初始化。类似,可以用不带static的代码块来对非static成员进行初始化(有匿名内部类时不得不这么写。)

java的命名空间

c++中试图用namespace解决命名冲突,java中则采用倒转的域名来做包名,从而解决命名冲突。
目前包名统一为小写字母。
由于域名的每个dot表示子目录,这种方式会形成较深的目录结构和大量空目录。好在ide可以代劳。
java会在CLASSPATH下根据文件开头的package信息(比如com.demo)来找。

java不需要函数声明

在java中,不需要函数声明。只要后面有函数定义,前面就能使用这个函数。

静态变量和方法的访问

类中的静态变量和方法,可以通过类名访问,也可以通过对象名访问。

构造函数

构造函数的名字和类名完全相同。所以构造函数的首字母是大写的,而其它方法的首字母是小写的。
在C++中,无参构造函数称为默认构造函数。
如果没写构造函数,编译器会帮你安排一个默认无参构造函数。如果写了,就不会安排。
可以在构造函数中调用其它构造函数,以减少重复代码。

.java,.class,.jar

一个.java文件称为一个编译单元,其中应当有一个跟文件同名的类,且其中只有一个public类。
言外之意是一个.java文件中可以有多个类。由于一个.class文件对应一个类,所以编译生成的.class文件会比.java文件多。所有的.class文件会被打包成jar文件。

比较变量是否相等

==和!=比较的是两个reference的值。对于小整数Integer,结果为true。对于大整数则不然。大小的界限似乎是-127-128。原因是缓存。书中没提,亲测如此。
使用equals判断对象内容是否相同的时候,记得要先定义class的equals方法,默认的equals是判断reference是否相同

方法签名与重载

方法名称和参数列表共同组成方法的签名。
重载只需要保证参数类型列表不同即可,即使只是交换顺序也行。
书中讨论了基本类型做参数重载时的宽度和转型问题(比如同名函数的int和long版本),此处不详述。
重载不考虑返回值,因为如果参数列表相同而仅返回值不同,是无法确认是哪个函数的。函数签名也是不考虑返回值的。

gc机制

gc只知道如何回收new出来的内存,如果不是new出来的(唯一的例子是,native method,也就是调用了C/C++代码的时候),需要在gc之前调用finalize,finalize需要自己实现。
finalize其实也有其它作用,比如做一些检查。书中的例子是,在finalize中检查book是否check in(业务需求是,不应该回收没有check in的book)。
finalize与C++的析构函数不同。在C++中,每一个对象都一定会被destroy。在Java中,不是所有的对象都会被回收(只有JVM认为空间不太够的时候,才会开始回收和finalize,这样可以减少不必要的开销)。实际上回收不是destroy。回收只管存储。而destroy的范围更广。
finalize不应被显式直接调用。
Joshua Bloch goes further in his section titled “avoid finalizers”:“Finalizers are unpredictable, often dangerous, and generally unnecessary.” ,Effective Java Programming Language Guide
通过System.gc可以强制调用gc。

gc机制可以加快内存分配,这使得Java中创建object比C++程序员想象中要更快。
书中的比方是,C++的堆内存分配就像庭院划分草皮,而Java的堆内存分配就像传送带。Java中有类似heap pointer的东西,就好像栈顶指针一样。

@override

首先,这是注释。为什么要有这个注释,给谁看?
这句注释告诉编译器你的目的,函数名称没有写错,我知道我是要重写一个已经有了的函数。注意,试图override的时候,signature必须完全一样,否则会出现父子一起重载的怪像。如果加了@override,那么在你搞错signature的时候,编译器会提醒你,你是在重载而不是在覆写。
此外,也可以提示其它程序员。

成员与方法的可见性

protected成员子类可以访问。如果不声明可见性,默认使用package access(或称为friendly),也就是同一个package下的视为public,不同package下的视为private。
如果若干java文件位于同一目录下,且它们都没有显式指定package,那么它们被认为同属于一个default package。因为默认包的类没有包名, 在被有包结构的类引用时,会被当成本包内的类,就会无法识别。应该养成建立包结构的习惯。
在这里插入图片描述
子类中继承的成员不能拥有比原先赋值的权限更低的访问权限,只能拥有更高的访问权限。private成员是无法继承的。
一种编程习惯是public在最前,private在最后,这样读者就可以轻松避开内部实现。

一般建议不要有protected类型的field,实现同样的功能最好用private的field配合protected的set方法。

类的可见性

类要么是public,要么是package access(仅内部类可以为private或protected)。
如果类是private的,那么没有任何其它类能访问它。要想实现类似的效果(除了我,谁都不能造,想造只能通过我),有两个方法,都需要将其构造器显式声明为private:
要用时请我做一个:在该类内提供一个static方法,在static方法中调用自己的构造函数并返回给请求者。
做好一个,要用时找我借:在该类内提供一个new好的static的该类实例,需要时返回请求者。
第二种方法是单例模式的实现。

如果构造方法是private的,那么这个类无法被继承。

关于main函数

同一个java文件中可以有多个类,每个类中都可以有main函数。
只要通过java 类名即可指定该类下的main函数。类的main函数可以用于类的测试,不必删除。如果不希望发布的产品中出现测试代码,也可以采用下面的方法,这样Tester会单独生成一个class文件,发布时不要这个class文件即可。

public class TestBed {
	public void f() { System.out.println("f()"); }
	public static class Tester {
		public static void main(String[] args) {
			TestBed t = new TestBed();
			t.f();
		}
	}
}

即使类是package access的,其main函数是public的。

final关键字

final关键字在不同的语境下有不同的含义。一般是“不可变”的意思。

final修饰数据

final用于修饰data时,可以表示常数。在编译期间即可确认,并可提前计算,以减少运行耗时。注意,如果使用final来声明data,需要提供值(要么当行提供,要么在构造函数中提供)。
final用于修饰一个reference时,reference本身不变,但不保证reference指向的对象不被修改。Java语言本身不支持一个“不变”对象(不过可以自己实现)。

final修饰参数

如果一个输入参数是final的,那么这个函数不能修改这个参数。

final修饰方法

确保子类继承后不能再重写这个方法。出于设计目的,希望一个方法的内容不被修改。
在早期,final方法会得到inline优化。不过据说现在不这么干了,作者认为改善甚微。
private方法是不会被重写的(You haven’t overridden the method, you’ve just created a new method.),这一点可以通过添加@override来求证(error: method does not override or implement a method from a supertype),所以final+private相比private,不会有什么额外效果。

final修饰类

这个类无法被继承。类中的属性可以为final的,也可以不为final。

类的加载

构造函数也算是static方法。
一个类只有在static成员被首次访问时才加载(尽可能地lazy)。一个例外是static final的常数,不会引起类加载。
编译后的.class在需要时才会被加载。

static对象和代码块的加载

按照代码顺序进行加载,只加载一次。

考虑继承时,初始化全过程

首先,仍然是“使用时加载”的原则;
加载一个类时,如果它有父类,则也加载父类,依次类推;
当不再有父类时,辈分最高的类进行static初始化,然后是其子类,依此类推(因为子类的static初始化依赖于父类的static初始化);
做好上述准备后,创建类的对象,首先是基本类型和对象类型赋默认初值;
递归调用父类的构造函数;
成员按代码赋初值;
调用构造函数的剩余部分。

总结一下:
代码加载;static初始化;分配置零内存;父类构造函数;本类成员初始化;本类构造函数。

多态(又名动态绑定/运行时绑定/晚绑定)

inheritance enabled you to treat an object as its own type or its base type. This treats many types (derived from the same base type) as if they were one type, and a single piece of code works on all those different types equally.
我的理解是,继承是子类替换父类,多态是兄弟类(甚至叔侄)互相替换。
以父类为入参时,我们无法确定入参到底是父类,还是父类的哪一个子类,只有运行时才能确定。这就是动态绑定。这样可以显著减少函数的数量(不用为每一个子类类型写一个函数),有点“一生万物”的感觉。

除了减少代码量
多态的好处还有易扩展,可以在不修改以父类为参数的函数的情况下,自由添加子类,乃至子类的子类(比如shape的draw的例子,不仅可以添加三角形,还可以添加三角形的子类等边三角形,而shape基类的draw方法始终无需修改)。

除了static方法(构造函数’re actually static methods, but the static declaration is implicit)和private方法,Java中所有方法都默认动态绑定(不像C++要声明virtual)。

构造函数中应该避免使用当前类还没有初始化的东西,避免使用当前类的方法。构造函数中唯一安全的函数是父类中的final/private函数。

多态的例外

如果函数只是返回父类的一个public成员,那么就不存在动态绑定。
In this example, different storage is allocated for Super.field and Sub.field. Thus, Sub actually contains two fields called field: its own and the one it gets from Super. However, the Super version is not the default that is produced when you refer to field in Sub. To get the Super field you must explicitly say super.field.
子类中不应该再声明一个与父类public成员同名的成员。如果的确这么做了,那么默认使用子类的。

object类的方法

方法名作用
protected Object clone()创建并返回此对象的一个副本
boolean equals(Object obj)指示其他某个对象是否与此对象“相等”
protected void finalize()当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
Class<?> getClass()返回此 Object 的运行时类
int hashCode()返回该对象的哈希码值
void notify()唤醒在此对象监视器上等待的单个线程
void notifyAll()唤醒在此对象监视器上等待的所有线程
String toString()返回该对象的字符串表示
void wait()在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待
void wait(long timeout)在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待
void wait(long timeout, int nanos)在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待

toString的默认实现:类名+hashCode()生成的的无符号十六进制数
equal的默认实现:比较两个对象的地址

如何写一个自己的equal函数

  1. If the rval is null, it’s not equal.
  2. If the rval is this (you’re comparing yourself to yourself), the two objects are equal.
  3. If the rval is not the same class or subclass, the two objects are not equal.
  4. If all the above checks pass, then you must decide which fields in the rval are important (and consistent), and compare those.
    依照上述原则的代码示例:
public boolean equals(Object rval) {
	if(rval == null)
		return false;
	if(rval == this)
		return true;
	if(!(rval instanceof Equality))
		return false;
	Equality other = (Equality)rval;
	if(!Objects.equals(i, other.i))
		return false;
	if(!Objects.equals(s, other.s))
		return false;
	if(!Objects.equals(d, other.d))
		return false;
	return true;
}

guava

接近生产代码的demo

public class NonNullConstruction {
	private Integer n;
	private String s;
	NonNullConstruction(Integer n, String s) {
		this.n = checkNotNull(n);
		this.s = checkNotNull(s);
	}
}

接近教科书的demo

import java.util.function.*;
import static com.google.common.base.Preconditions.*;

public class GuavaPreconditions {
	static void test(Consumer<String> c, String s) {
	//我所理解的Consumer<String>就是一个以string为入参的函数
	//accept,就是将参数传给consumer,并执行consumer的内容。
		try {
			System.out.println(s);
			c.accept(s);
			System.out.println("Success");
		} catch(Exception e) {
			String type = e.getClass().getSimpleName();
			String msg = e.getMessage();
			System.out.println(type + (msg == null ? "" : ": " + msg));
		}
	}

	public static void main(String[] args) {
	//test的三种重载,由于效率问题,第三种的replace限定为字符串类型
		test(s -> s = checkNotNull(s), "X");
		test(s -> s = checkNotNull(s, "s was null"), null);
		test(s -> s = checkNotNull(s, "s was null, %s %s", "arg2", "arg3"), null);
	// checkArgument的使用
		test(s -> checkArgument(s == "Fozzie"), "Fozzie");
	// checkState的使用
		test(s -> checkState(s.length() > 6), "Mortimer");
	// checkElementIndex, checkPositionIndex, checkPositionIndexes的使用
	// 前两者区别在于小于和小于等于,第三者则是下限非负,上限小于等于
		test(s ->checkElementIndex(6, s.length()), "Robert");
		test(s ->checkPositionIndex(6, s.length()), "Robert");
		test(s -> checkPositionIndexes(0, 6, s.length()), "Hieronymus");
	}


Annotation

注解的优势在于整洁。
注解是一种元数据。趋势是,将元数据与源代码放在一起,而不是将元数据放在外部文件中。
Annotation的设计初衷,就是能生成描述性文件(如javadoc,以及一些框架需要外部xml),以及能生成新的类从而减少套路代码(如lombok)。
从使用上来说,注解类似public、void这样的modifier。

从定义上来说,注解类似接口。而且注解在编译后也会生成.class。区别在于,需要指定target(方法或field)和retention(源代码,class,运行时)等Meta-annotation(也就是注解的注解,共有五个)。

import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
	int id();
	String description() default "no description";
}

此处的id和description,就是我们见到的annotation括号里可以被赋值的属性。属性的类型比较有限,基本类型、字符串、class、枚举、注解、array。通过id()就能取到id。

为什么叫元数据?当注解用于方法的时候,感觉并不贴切,但当注解用于field、局部变量的时候,就是对数据的一种描述了:

public class Member {
	@SQLString(30) String firstName;
	@SQLInteger Integer age;
	@SQLString(value = 30,
constraints = @Constraints(primaryKey = true))
String reference;
	//...

上面代码的完整版可以通过自己写的annotation processor来自动生成建表sql语句。
通过继承AbstractProcessor,原书实现了一些processor。通过javac可以调用这些processor,可以看见生成的class。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值