Java编程思想读书笔记(15-17)

Java编程思想读书笔记(15-17)

第15章 泛型

​ 一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的舒服就会很大。

​ 泛型实现了参数化类型的概念,是代码可以应用于多种类型。“泛型”这个术语的意思是:“适用于许多许多的类型”。

1、简单泛型

​ 泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。

public class Holder<T> {

	private T a;

	public Holder(T a) {
		this.a = a;
	}

	public T getA() {
		return a;
	}

	public void setA(T a) {
		this.a = a;
	}

	public static void main(String[] args) {
		Holder<String> h = new Holder<String>("Hello");
		String hello = h.getA();
		System.out.println(hello);
	}
}

​ 创建泛型类对象的时候必须指明想持有什么类型的对象,将其置于尖括号内。

​ 这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。

2、一个元组类库

​ 仅一次方法调用就能返回多个对象,但是return语句只允许返回单个对象,因此,解决办法就是创建一个对象,用它来持有想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。有了泛型,我们可以一次性的解决该问题,以后再也不用在这个问题上浪费时间了。同时,我们在编译器就能确保类型安全。

​ 这个概念成为元组,它是将依据对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。

​ 一个二维元组如下:

public class TwoTuple<A, B> {
	public final A first;
	public final B second;

	public TwoTuple(A a, B b) {
		first = a;
		second = b;
	}

	@Override
	public String toString() {
		return "TwoTuple{" +
				"first=" + first +
				", second=" + second +
				'}';
	}
}

​ 可以使用继承的方式去实现长度更长的元组。

3、使用泛型实现简单的链式存储
public class LinkedStack<T> {

	private Node<T> top = new Node<T>();

	public static void main(String[] args) {
		LinkedStack<String> lss = new LinkedStack<String>();
		for (String s : "Phasers or stun!".split(" ")) {
			lss.push(s);
		}
		String s;
		while ((s = lss.pop()) != null) {
			System.out.println(s);
		}
	}

	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;
	}

	private 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;
		}
	}
}
4、泛型接口

​ 接口使用泛型与类使用泛型没有什么区别

5、泛型方法

​ 泛型方法所在的类可以不是泛型类。

​ 泛型方法是的该方法能够独立与类而产生变化。一个基本的指导原则是:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法。

​ 定义泛型方法如下:

public class GenericMethods {
	public static void main(String[] args) {
		GenericMethods gm = new GenericMethods();
		gm.f("");
		gm.f(1);
		gm.f(1.0);
		gm.f(1.0f);
		gm.f('c');
		gm.f(gm);
	}

	public <T> void f(T x) {
		System.out.println(x.getClass().getName());
	}
}

结果如下:
在这里插入图片描述

​ 在使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明类型参数,因为编译器会为我们找出具体的类型。这称为类型参数推断。因此,我们可以向调用普通方法一样调用f(),而且就好像f()时被无线次的重载过。

6、显式的类型说明

​ 在点操作符与方法名之间插入尖括号。

New.<Persion,List<Pet>>map());
7、可变参数与泛型方法
public class GenericVarargs {

	public static <T> List<T> makeList(T... args) {
		List<T> result = new ArrayList<T>();
		for (T item : args) {
			result.add(item);
		}
		return result;
	}

	public static void main(String[] args) {
		List<String> ls = makeList("A");
		System.out.println(ls);
		ls = makeList("A", "B", "C");
		System.out.println(ls);
		ls = makeList("ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""));
		System.out.println(ls);
	}
}

结果如下:
[外链图片转存失败(img-x2h4rtRg-1569160649289)(media/1569145090220.png)]

8、擦除的神秘之处

​ 在泛型代码内部,无法获得任何有关泛型参数类型的信息。

​ 因此你可以知道诸如类型参数标识符和泛型类型边界这类的信息——你却无法知道用来创建某个特定实例的实际的类型参数。

​ Java泛型时使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此List和List在运行时实际上时相同的类型。这两种形式都被擦除成他们的原生类型,即List。

9、泛型边界
T extends Class

​ 边界<T extends HasF>声明T必须具有类型HasF或者从HasF导出的类型。

​ 泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。T 擦除到HasF,就好像在类的声明中用 HasF 替换T 一样。

? extends T

​ ? 是泛型表达式中的通配符。 ? extends T 表示: T 数据类型或着 T 的子类数据类型

? super T

​ ? super T 表示: T 数据类型 或着 T的超类数据类型, super 表示超类通配符。

? extends T 表示消费者 list 然后把里面的数据消费掉。 ? super T 表示生产者 传入一个list 然后往里面添加数据,进行其他操作。

第16章 数组

1、数组的特殊之处
  • 数组的效率比容器要高很多。
  • 数组可以真正持有某种具体的类型,这是容器在泛型之前所没有的。
  • 数组可以持有基本类型。

但是泛型出现之后,数组仅存的优点就是效率了。

2、数组是第一级对象
  • 数组的标识符只是一个引用,只想在堆中创建的一个真实对象,这个对象用以保存只想其他对象的引用。
  • 只读成员length是数组对象的一部分,标识此数组对象可以存储多少元素。
  • []语法是访问数组对象的唯一方式。
  • 数组中的对象会被默认初始化为null,值会被初始化为0
3、返回一个数组

​ Java可以使用数组作为返回值。

4、多维数组

​ 创建多维数组很方便。对于基本类型的多维数组,可以通过使用花括号将每个向量分隔开:

public class MultidimensionalPrimitiveArray {
	public static void main(String[] args) {
		int[][] a = {
				{1, 2, 3},
				{4, 5, 6}
		};
		System.out.println(Arrays.deepToString(a));
	}
}

​ 也可以使用new关键字来创建数组:

public class MultidimensionalPrimitiveArray {
	public static void main(String[] args) {
		int[][][] a = new int[2][2][4];
		System.out.println(Arrays.deepToString(a));
	}
}

​ 结果如下:
在这里插入图片描述

​ 可以看到基本类型数组的值在不进行显示初始化的情况下,会被自动初始化。对象数组挥别初始化为null;

5、数组与泛型

​ 不能实例化具有参数化类型的数组:

Peel<Banana>[] peels = new Peel<Banana>[10];//这是错误的

擦除会移除参数类型信息,而数组必须知道他们所持有的确切类型,一强制保证类型安全。

  • 可以参数化数组本身的类型。
  • 可以创建非泛型的数组,然后将其转型:
List[] la = new List[10];
ls = (List<String>[])la;
6、Arrays实用功能

System.arraycopy():用于复制数组,比使用for循环复制要快很多。

Arrays.equals():用来比较整个数组,数组相等的条件是元素个数必须相等,并且对应位置的元素也相等。

Arrays.sort():对数组中元素进行排序,只要数组中的元素实现了Comparable接口,或者提供一个Comparator。字符串的排序按照字典序进行排序,可以使用String.CASE_INSENSITIVE_ORDER来忽略大小写。

Arrays.binarySearch():对已排序的数组进行二分查找,如果数组未排序可能会无限循环。

第17章 容器深入研究

1、完整的容器分类

在这里插入图片描述
Java SE5新添加了:

1、Queue接口(LinkedList已经为实现该接口做了修改)及其实现PriorityQueue和各种风格的BlockingQueue。
2、ConcurrentMap接口及其实现ConcurrentHashMap,用于多线程机制
3、CopyOnWriteArrayList和CopyOnWriteArraySet,用于多线程机制
4、EnumSet和EnumMap,为使用enum而设计的set和map的特殊实现
5、在Collections类中的多个便利方法
虚线框表示abstract类,它们只是部分实现了特定接口的工具。例如,如果要创建自己的Set,那并不用从Set接口开始并实现其中全部方法,只需从AbstractSet继承,然后执行一些创建新类必须的工作。

2、填充容器

​ 容器也有类似Arrays.fill()的方法来填充容器。通Arrays一样只复制同意对象引用来填充整个容器,并且支队List对象有用,但是所产生的列表可以传递给构造器或addAll()方法。

  • addAll()方法将一些元素添加到一个Collection中
  • nCopies()方法复制一些元素到Collections中,返回一个List集合
  • fill()方法复制元素填充到集合当中去
3、使用Abstract类

​ Abstract类提供了该容器的部分实现,如果你想要定制Collection和Map实现,这时你需要做的只是去实现那些产生想要的容器所必需的方法。如果所产生的容器是只读的,就像他同通常的测试数据那样,那么你需要提供的方法数量将减少到最少。

4、Collection的功能方法

​ 下图为可以通过Collection执行的所有操作。他们也是可以通过Set或List执行的所有操作。

在这里插入图片描述
​ 平时使用时大多将ArrayList向上转型为Collection来使用,所以只是用到了Collection的接口,ArrayList另有专有的接口。

5、可选操作

​ 执行各种不同的添加和移除的方法在Collection接口中都是可选操作。这意味着实现类并不需要为这些方法提供功能定义。

6、List的功能方法

List接口:

  • get(int); 获取指定索引位置的列表元素
  • set(int,E); 设置指定索引位置的列表元素
  • add(int,E); 将指定的元素添加到此列表的索引位置。
  • remove(int); 移除指定索引位置的元素;
  • indexOf(Object); 从列表头部查找Object对象元素
  • lastIndexOf(Objcet); 从列表尾部查找Object对象元素
  • listIterator(); 返回列表迭代器
  • listIterator(int); 返回指定索引位置的列表迭代器
  • subList(int,int); 该方法返回的是父list一个视图,从fromIndex(包含),到toIndex(不包含)
7、Set和存储顺序

​ Set的存储顺序在Set的不同实现之间会有所变化,因此,不同的Set实现不仅具有不同的行为,而且它们对于可以在特定的Set中放置的元素的类型也有不同的要求:
在这里插入图片描述
​ HashSet上的星号表示:如果没有其他的限制,这就是你的默认选择,因为他对速度进行了优化。

​ 必须为散列存储和树形存储都创建一个equals()方法,但是hashCode()只有在这个类将会别置于HashSet或者LinkedHashSet中时才是必须的。但是,对于良好的编程风格而言,你应该再覆盖equals()方法是,总是同时覆盖hashCode()方法。

8、队列

​ 除了并发应用,Queue仅有的两个实现时LinkedList和PriorityQueue,他们的差异在于排序行为而不是性能。

Queue的接口:

  • boolean add(E e); 添加一个元素,添加失败时会抛出异常
  • boolean offer(E e); 添加一个元素,通过返回值判断是否添加成功
  • E remove(); 移除一个元素,删除失败时会抛出异常
  • E poll(); 移除一个元素,返回移除的元素
  • E element(); 在队列的头部查询元素,如果队列为空,抛出异常
  • E peek(); 在队列的头部查询元素,如果队列为空,返回null
  • Queue在Java SE5中仅有的两个实现是LinkedList和PriorityQueue,它们的差异在于排序行为而不是性能
优先级队列

​ 列表中每个对象都包含一个字符串和一个主要的以及次要的优先级值。该列表的排序也是通过实现Comparable而进行控制的。

Queue<String> queue=new PriorityQueue<>();
双向队列

​ 双向队列就像一个队列,但是可以在任意一段添加或移除元素。在LinkedList中包含支持双向队列的方法,但在Java标准类库中没有任何显式的用于双向队列的接口。因此,LinkedList无法去实现这样的接口,无法像前面转型到Queue那样向上转型为Deque。但是可以使用组合创建一个Deque类,并直接从LinkedList中暴露相关方法:

  • queue.addFirst(); 向队列首部添加元素
  • queue.addList(); 向队列尾部添加元素
  • queue.getLast(); 获取队列尾部元素
  • queue.getFirst(); 获取队列首部元素
  • queue.removeFirst(); 移除队列首部元素
  • queue.removeLast(); 移除队列尾元素
  • queue.size(); 返回队列大小
9、理解Map

​ Map的几种基本实现,包括:HashMap,TreeMap,LinkedHashMap,WeakHashMap,ConcurrentHashMap,IdentityHashMap。他们都有同样的基本接口Map,但是行为特性各不相同,这表现在效率、键值对的保存及呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判定”键“等价的策略等方面。

Map的接口如下:

  • size(); 返回Map大小
  • isEmpty(); 判断Map集合是否为空
  • containsKey(); 判断Mpa集合是否包括Key键
  • containsVaule(); 判断Map集合是否包括Vaule
  • get(Object); 获得Map集合键为Object的Vaule
  • put(K,V); 添加一个K,V键值对
  • remove(Object); 移除K键值对
  • putAll(Map); 添加所有元素Map集合
  • clear(); 清空Map中所有集合元素
  • keySet(); 返回键Key的Set集合
  • values(); 返回值Vaule的Set集合
  • entrySet(); 返回Map.entrySet集合
  • remove(Objcet,Object); 移除K,V集合
  • replace(K,V,V); 替换键值对
性能

​ 性能是映射表中的一个重要问题,当在get()中使用线性搜索时,执行速度会相当的慢,而这正式HashMap提高速度的地方。HashMap使用了特殊的值,叫做散列码,来取代对键的缓慢搜索。散列码是”相对唯一“的、用以代表对象的int值,他是通过将该对象的某些信息进行转换而生成的。hashCode()是根类Object中的方法,因此所有的Java对象都能产生散列码。HashMap就是适用对象的hashCode()进行快速查询的每次方法能够显著提高性能。

下面是Map的基本实现:
在这里插入图片描述

10、散列与散列码
理解hashCode()

​ 使用散列的目的在于:想要使用一个对象来查找另一个对象。不过使用TreeMap或者你自己实现的Map也可以达到此目的。

为速度而散列

​ 创建一种新的Map并不困难。但是很难做到高性能。

​ 散列的价值在于速度:三烈士的查询得以快速进行。由于瓶颈为于建的查询速度,因此解决方案之一就是保持间的排序状态,然后使用Collections.binarySearch()进行查询。

​ 散列则更进一步,他将键保存在某处,以便能够很快找到。存储一组元素最快的数据结构师叔祖,所以用它来表示键的信息(不是键本身)。

11、选择接口的实现

​ Hashtable、Vector和Stack的”特征“是,它们是过去遗留下来的类,目的只是为了支持老的程序

​ 不同类型的Queue只在他们接受和产生数值的方式上有所差异

​ ArrayList底层由数组支持;而LinkedList是由双向链表实现的。如果要经常在表中插入或者删除元素,LinkedList就比较合适;否则就应该使用速度更快的ArrayList。

​ HashSet是最常用的,查迅速的最快;LinkedHashSet保持元素的插入的次序;TreeSet基于TreeMap,生成一个总是排序状态的Se。

12、实用方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
shtable、Vector和Stack的”特征“是,它们是过去遗留下来的类,目的只是为了支持老的程序

​ 不同类型的Queue只在他们接受和产生数值的方式上有所差异

​ ArrayList底层由数组支持;而LinkedList是由双向链表实现的。如果要经常在表中插入或者删除元素,LinkedList就比较合适;否则就应该使用速度更快的ArrayList。

​ HashSet是最常用的,查迅速的最快;LinkedHashSet保持元素的插入的次序;TreeSet基于TreeMap,生成一个总是排序状态的Se。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值