On_Java Basic_Edition Notes
On _ Java
08_复用
8.8.1 final 数据
对于基本类型,final使其值恒定不变。但对于对象引用,final使其引用恒定不变。
一旦引用被初始化一个对象,他就永远不能被更改为指向另一个对象了。
class Test{
Test(String s){
System.out.println(s);
}
}
final Test t = new Test("One");
// t = new Test("Tow");
// t已经不能被第二次引用了,因为声明它的时候,修饰他的有final存在。
8.8.2 final 方法
只有在明确防止重写的情况下才创建一个final方法。
final与private:类中的任何private方法都是隐式的final方法。
这是因为你不能访问一个private方法,也不能重写他。
重写只有在方法是基类接口的一部分时才会发生。也就是说,必须能将一个对象向上转型为其基类类型并能调用与其相同的方法。
如果一个方法是private的,他就不是基类接口的一部分。
如果你重写了一个private的方法,实际上你并没有重写他,你只是重新写了一个名字一样的方法罢了。
8.8.3 final 类
将整个类定义为final时,就阻止了该类的所有继承。
这样做是因为,出于某种原因,你希望自己对这个类的设计永远不要被修改;或者出于安全考虑,你不希望它有子类。
09_多态
9.2.1 方法调用与绑定
绑定发生在运行时,并基于对象的类型。
后期绑定也成为动态绑定或者是运行时绑定
当一种语言实现后期绑定时,必须有某种机制在运行时来确定对象的类型,并调用恰当的方法。
后期绑定机制因语言而异,但可以想象,必须要有某种类型的信息放在对象里。
Java中的所有方法都是后期绑定,除非方法是static或final的(private隐性为final)这意味着通常不需要你来决定是否要执行后期绑定--它会自动发生。
自己的理解:
动态绑定的方法就是多态方法,就是被重写的方法。
@Override 的方法
如果在构造器内调用动态绑定的方法,就会用到该方法被重写后的定义 << 这很恐怖,兄弟们 >>
class Glyph{
void draw(){
System.out.println("Glyph.draw()")
}
Glyph(){
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() before draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
System.out.println("RoundGlyph.RoundGlyph,radius = "+radius);
}
@Override void draw(){
System.out.println("RoundGlyph.draw(),radius = "+radius);
}
public static void main(String[] args){
new RoundGlyph(5);
}
}
9.2.5 陷阱:字段与静态方法
字段与static方法,他们都没有多态可言。是什么类型就调用哪个类里面的相应方法
如果一个方法是静态的,那它的行为就不会是多态的。
class JAVA_test{
static void Prin(){
System.out.println("Prin(1)");
}
}
class Java_lll extends JAVA_test{
static void Prin(){
System.out.println("Prin(2)");
}
}
class main_1{
public static void main(String s){
JAVA_test jav = new Java_111();
jav.Prin();
}
}
输出:Prin(1)
9.5 用继承进行设计
真的,这段代码太优雅了,我真的感觉他是在写一首诗。没事了可以看一眼。
我认为这段Code会深深的影响我以后写代码的节奏。。。太优雅了,太优雅了。
9.5.2 向下转型与反射
尝试向下转型时,如果类型正确就会成功,否则就会得到一个ClassCastException的异常。
10_接口
下面是复习前面的内容
我们创建抽象类,是想通过一个公共接口来操作一组类。
包含抽象方法的类称为抽象类,如果一个类包含一个或多个抽象方法,则该类必须被定义为抽象类,否则编译器会产生错误消息。
如果一个新类型继承了抽象类,并希望能生成自己的对象,那它必须为基类中的所有抽象方法提供方法定。
如果不这样做,(你可能会选择不这样做)那么子类也会是抽象的,编译器将强制你使用abstract关键字来设定这个子类。
一个抽象类可以不包含任何抽象方法。如果一个类并不需要包含抽象方法,但同时还想阻止对它的任何实例化,这时将其定义为抽象类就很有用了。
请记住,默认的访问权限是 “友好的” 你很快就会了解到,接口自动将其方法设为了public。事实上,接口里只允许有public方法或者protected方法。
抽象类则几乎对访问权限没有什么限制。
10.2 接口定义
与类一样,你可以在interface关键字之前添加public关键字,(前提是与文件名一样的接口)如果没有public,将会获得包权限,只能在一个包里面使用。
接口也可以包含字段,不过这些字段全部都是隐式的final和static
default 字段可以在interface里面定义方法体,实现了此接口的类都可以调用。
public interface Test_An {
void first_demo();
void second_demo();
default void sout(){
System.out.println("You can test..");
}
}
如上面的sout方法,凡是实现了此接口的类都可以将其调用,而且不用重写这个方法。
10.2.2 多重继承
当若干个interface中的若干个方法名称一样时,会触发编译错误,有一个办法可以将这几个名字一样的方法区分开哦。
虽然具有相同的名称,但是可以根据签名来区别他们的不同,什么是签名?
签名就是名称和参数类型
返回类型并不属于签名
10.2.3 接口中的静态方法
package None;
class Heat implements Operation{
@Override
public void execute() {
System.out.println("Heat");
}
}
class MetalWork{
public static void main(String[] args) {
Operation twist = new Operation() {
@Override
public void execute() {
System.out.println("Twist");
}
};
Operation.runOps(
new Heat(),
new Operation() {
@Override
public void execute() {
Operation.show("Hammer");
}
},
twist::execute,
()->Operation.show("Anneal")
);
}
}
interface Operation {
void execute();
static void runOps(Operation... ops) {
for (Operation op :
ops) {
op.execute();
}
}
static void show(String msg){
System.out.println(msg);
}
}
在这里,可以看到创建Operation的不同方式。
-
常规类heat
-
匿名类
-
方法引用
-
Lambda表达式--需要最少的代码
10.4 完全解耦
package fun.tanghe;
import java.util.Arrays;
class Processor {
public String name() {
return getClass().getSimpleName();
}
public Object process(Object input) {
return input;
}
}
class Upcase extends Processor {
@Override
public Object process(Object input) {
return ((String) input).toUpperCase();
}
}
class Downcase extends Processor {
@Override
public Object process(Object input) {
return ((String) input).toLowerCase();
}
}
class Splitter extends Processor {
@Override
public Object process(Object input) {
return Arrays.toString(((String) input).split(" "));
}
}
public class Applicator {
public static void apply(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
public static void main(String[] args) {
String s = "We are such stuff as dreams are made on";
apply(new Upcase(), s);
apply(new Downcase(), s);
apply(new Splitter(), s);
}
}
apply方法里面定义的是算法的固定部分,而策略包含了变化的部分。(这就是策略设计模式)
10.5 组合多个接口
如果可以在没有任何方法定义或成员变量的情况下创建基类,那么就使用接口而非抽象类。事实上,如果你认为某个类可以作为基类的话,也就可以重新考虑把它设计为接口。
10.6 通过继承来扩展接口
interface A{
void A1();
}
interface B{
void B1();
}
interface C extends A,B{
void C1();
}
class D implements C{
@Override
public void A1() {}
@Override
public void B1() {}
@Override
public void C1() {}
}
通常,只能将extends用于单个类。但是在构建新接口时,extends可以关联多个父接口(base interface),注意接口名称用逗号分隔。
10.9 嵌套接口
接口可以嵌套在类和其他接口中
详细见252页,
private的嵌套接口有什么好处,你有可能会猜测,它只能作为DImp中的私有内部类来实现,但是A.DImp2
表明它也可以作为public类来实现
不过,A.DImp2在使用时只能被视为自身的类型,你不能提及它实现了private的接口D。
所以实现了private接口的话,可以在不添加任何类型信息的情况下,限定该接口中的方法定义
(也就是说,不允许向上转型为它的接口类型,不允许任何向上转型)
11 内部类
11.6匿名内部类
如果你正在定义一个匿名类,而且一定要用到一个在该匿名类之外定义的对象
编译器要求参数引用用final修饰,或者是“实际上的最终变量”
eg.
interface Destination{
String readLabel();
}
public class Per{
public Destination destination(String dest,float price){
return new Destination(){
private int cost;
{// [1]
cost = Math.round(price);
if (cost > 100){
System.out.println("Over budget!");
}
}
private String label = dest;
@Override
public String readLabel() {
return label;
}
};
}
}
这里解释一下 [ 1 ] 标记的大括号:它类似于正常类的构造函数。属于是初始化等级的作物。
11.7 嵌套类
所谓嵌套类啊,就是内部类的声明前面加了一个static
,这样就可以Outer.Inner object = new Outer.Inner();
有可能会很迷,这有什么奇怪的呢?
如果没有这个static,就需要先new外面的外部类,才能用.new()
语法将内部的new出来。
11.7.2 从多层嵌套的内部类中访问外部成员
一个内部类被嵌套多少层并不重要。它可以透明地访问包含它的所有类的所有成员。
如下面代码所示
public class Parcel11 {
private void f() {
}
class A {
private void g() {
}
public class B {
void h() {
g();
f();
}
}
}
public static void main(String[] args) {
Parcel11 parcel11 = new Parcel11();
A a = parcel11.new A();
A.B b = a.new B();
b.h();
}
}
就像上面代码表示的,private的方法依旧可以调用。
12 集合
12.3 添加一组元素
Arrays.asList()
方法可以接受一个数组,或者一个用逗号分隔的元素列表(使用可变参数),并将其转换为一个List对象
Collections.addAll()
方法接受一个Collection对象,一个数组,或者用一个逗号分隔的列表,将其中所有的元素都添加到这个Collection中。
Collections.addAll(target,1,2,3); //此时,(1,2,3)已经加到了target的后面。
Arrays.asList(1,2,3,4,5); //此时,Arrays添加进去的对象并不能增加和删除,因为它底层的实现依靠数组,数组是不能随时调整大小的。
12.6 迭代器
- 使用
iteration()
方法让一个collection返回一个Iteration。这个迭代器将准备好返回序列中的第一个元素。 - 使用
next()
方法获得序列中的下一个对象。 - 使用
hasNext()
方法检查序列中是否还有更多对象。 - 使用
remove()
方法删除该迭代器最近返回的元素。
迭代器统一了对集合的访问。
(记得日后要深究一下ListIteration
这个类型)。
12.7 LinkedList
和ArrayList
一样,LinkedList实现了基本的List接口。但是与ArrayList相比,他在List中间执行插入和删除的效率更高,不过随机访问操作的表现要更差一些。
LinkedList的方法:
getFirst()
和element()
一样,他们都返回列表的头部,而并不移除它。ps:还有一个peek()哦removeFirst()
和remove()
一样,他们都会返回且删除列表的头部。ps:还有一个poll()哦addFirst()
在列表的开头插入一个元素。offer()
,add()
,addLast()
都是相同的,都是向列表的尾部插入一个元素。removeLast()
移除并返回列表中的最后一个元素。
12.8 Stack
栈是一个后进先出的概念,详细可以参考叠起来的盘子,先放在上面的会被先拿走。
本书的这个部分没有看懂,日后再看。
12.9 Set
Set中不允许出现重复的对象值。
package fun.tanghe.All_other;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class SetOperations {
public static void main(String[] args) {
Set<Object> set1 = new HashSet<>();
Collections.addAll(set1, "A B C D E F G H I J K L".split(" "));
set1.add("M");
System.out.println(set1); //[0]
System.out.println("H:" + set1.contains("H")); //1
System.out.println("N:" + set1.contains("N")); //2
Set<Object> set2 = new HashSet<>();
Collections.addAll(set2, "H I J K L".split(" "));
System.out.println("set2 in set1:" + set1.containsAll(set2)); //3
set1.removeAll(set2);
System.out.println("set2 remove from set1:" + set1); //4
Collections.addAll(set1, "X Y Z".split(" "));
System.out.println("'X Y Z' added in set1" + set1); //5
}
}
12.10 Map
注意:Map和Collection没有关系,不是其子接口(也就是说不能创建流)。Collection是对象集合,Map是键值对集合。
Map的get()
方法:()里填写你想要查找的键,如果没有,将会返回null;
如果有你想要查找的值,它将会返回与之相对应的值。
put()
方法:这个方法是往里面添加元素用的。eg:map.put(key,value)
.
containsKey()
与containsValue()
,可以查看其中是否有包含某个键或者某个值。
类似于数组和Collection,Map也很容易扩展为多维;
12.11 新特性:记录(record)类型
这个类型为何出现:
要让一个类的对象可以用作Map(或者Set)中的键,我们必须为这个类定义两个函数:
equals()
和hashCode()
。
JDK16添加了record
关键字,定义为数据传输对象(也叫做数据载体)。
当使用record关键字时,编译器会自动生成:
- 不可变的字段。
- 一个规范的构造器。
- 每个元素都有的访问器方法。
- equals()
- hashCode()
- toString() ->会生成一个可读性非常好的结果。
我们不能继承record,因为它是隐含的final(而且不能为abstract)
。
此外,record也不能继承其他的类。然而,record可以实现Interface。
record Employee(String name,int id){}
var em_1 = new Employee("XiaoMing",12);
获取name,em_1.name()
。
获取id,em_1.id()
。
12.12 Queue
队列是一个典型的先进先出的集合,和排队是一样的,站到前面的肯定是先出去了。
换言之,我们在其中一端放入,在另一端拿出来,放入的顺序和拿出的顺序是一样的。
LinkedList
实现了Queue
接口,因此LinkedList可以作为一种Queue来使用。
PriorityQueue - 优先级队列
优先级队列是说,下一个要拿出来的元素是需求最强烈的(最高优先级)。
如果我们搭建了一个消息系统,那么有些消息要比其他消息更重要,而且无论它们什么时候到达的,都应该尽快处理。
优先级队列 - Priority
的出现为这个需求构建了自动化的处理。
默认的排序方法使用的是对象在队列中的自然顺序,但我们可以通过提供自己的Comparator来修改顺序。
PriorityQueue确保,当调用peek()
,poll()
,remove()
时,我们得到的是优先级最高的元素。
重要:
Integer,String和Character之所以能配合PriorityQueue,是因为这些类已经有自然顺序。如果想在PriorityQueue中使用我们自己的类,就必须包含额外的用来生成自然顺序的功能,或者提供自己的Comparator。ps:进阶卷第三章有一个更复杂的例子来说明这一点。
12.13 Collection 和 Iterator 的对比
Collection是所有序列集合共同的根接口。可以认为它是一个为表示其他接口之间的共性而出现的“附属接口”。
12.14 for-in 和 迭代器
能配合for-in使用是所有Collection对象的特征。
其原理是java5引入了一个叫做Iterable的接口,该接口所包括的iterable()
方法会生成一个Iterable。
for-in使用这个Iterable接口来遍历序列。因此,如果我们创建了任何一个实现Iterable接口的类,都可以将其用于for-in的语句中。
12.15 总结
- 如果要执行大量的随机访问,应该使用ArrayList;但是如果要在列表中执行大量的插入和删除操作,则应该使用LinkedList。
- 队列和栈的行为都是通过LinkedList提供的。
- Map将对象而非整型值与其他对象关联起来。HashMap是为快速访问而设计的,而TreeMap将它的键以有序的方式保存,所以不如HashMap快。LinkedHashMap按照元素的插入顺序来保存,但是通过Hash提供了快速访问的能力。
- 对于相同元素,Set只保存一个。HashSet提供了最快的查找速度,而TreeSet会以有序方式保存元素。LinkedHashSet会按照元素的插入顺序来保存。
- 不要在新代码中使用Vector,Hashtable和Stack等遗留类。
实际上只有四个基本的集合组件--Map、List、Set、Queue。每个组件只有两到三个实现。
13 函数式编程
13.2 lambda 表达式
任何lambda表达式的基本语法如下所示:
- 只有一个参数,可以只写这个参数,不写括号。不过这是一种特殊情况。
- 通常情况是用括号将参数包裹起来。为了一致性,在单个参数是也可以使用括号,尽管这并不常见。
- 在没有参数的情况下,必须使用括号来指示空的参数列表。
- 在有多个参数的情况下,将它们放在使用括号包裹起来的参数列表内。
- 如果lambda表达式需要多行代码,则必须将这些代码行放在花括号中。这种情况下又需要使用return从lambda表达式生成一个值了。【一行的那种lambda表达式不需要return,否则会出现问题。】
递归
lambda表达式可以调用自己,但是前提是必须赋值给一个static变量,否则会出现编译错误。
13.3 方法引用
方法引用使用类名或对象,后跟 :: ,然后跟方法名。
package Functional;
interface Callable {
void call(String s); //[1]
}
class Describe {
void show(String msg) { //[2]
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) { //[3]
System.out.println("Hello," + name);
}
static class Description{
String about;
Description(String desc){
about = desc;
}
void help(String msg){ //[4]
System.out.println(about + " " + msg);
}
}
static class Helper{
static void assist(String msg){ //[5]
System.out.println(msg);
}
}
public static void main(String[] args) {
Describe d = new Describe();
Callable c = d::show; //[6]
c.call("Call()"); //[7]
c = MethodReferences::hello; //[8]
c.call("Bob");
c = new Description("valuable()")::help; //[9]
c.call("information");
c = Helper::assist; //[10]
c.call("Help!");
}
}
- 我们只从包含了一个方法的接口开始。
show()
的签名和Callable
中call()
的签名一致。hello()
的签名也与call()
一致。help()
是静态内部类中的一个非静态方法。assist()
是静态内部类中的一个静态方法。- 我们将
Describe对象
的一个方法引用赋值给了一个Callable
,Callable
中没有show()
方法,只有一个call()
方法。
然而,Java似乎对这种看似奇怪的赋值并没有意见,因为这个方法引用的签名和Callable
中的call()
方法一致。 - 现在可以通过调用
call()
来调用show()
,因为Java
将call()
映射到了show()
上。 - 这是一个静态方法的引用。
- 这是 [6] 的另一个版本:对某个活跃对象上的方法的方法引用,有时叫做“绑定方法引用”。
- 最后,获得静态内部类中的静态方法的方法引用,看起来就像在 [8] 处的外部类版本。
这还不是一个非常详尽的实例,我们很快就能看到方法引用的所有变种。
13.3.2 未绑定方法引用
未绑定方法引用指的是尚未关联到某个对象的普通(非静态方法)。对于未绑定引用,必须先提供对象,然后才能使用。
eg.
package fun.utree;
public class cvnn {
public static void main(String[] args) {
// one_1 ii = one::function(); 这句就会出错,因为function方法是一个普通的非静态方法。
one_2 iio = one::function; //这句就可以,因为方法的引用可以放在vvn方法里x的变量上。
iio.vvn(new one());//放入one。
}
}
class one {
void function() {
System.out.println("There is one");
}
}
interface one_1 {
void vvb();
}
interface one_2 {
void vvn(one x);
}
13.3.3 构造器方法引用
构造器本身就是static
的方法。因此引用构造方法与引用静态方法差不多。
Object::new;
public class CtorReference {
CtorReference(){
System.out.println("New success!");
}
}
interface o_opu{
CtorReference bb(); //返回的值为对象。
}
class ctor{
public static void main(String[] args) {
o_opu o_opu1 =CtorReference::new;
CtorReference bb = o_opu1.bb();
}
}
13.4 函数式接口
使用了@FunctionalInterface
注解的接口也叫做单一抽象方法。
因为要符合各种返回和输入的变量,接口的数量有所增长。
一般来说通过名字就可以知道或者了解特定的接口是干嘛的。下面是基本的命名规则:
- 如果接口只处理对象,而非基本类型,那就会用一个直截了当的名字,像Function、Consumer和Predicate等。参数类型会通过泛型添加。
- 如果接口接受一个基本类型的参数,则会用名字的第一部分来表示,例如LongConsumer、DoubleConsumer、IntPredicate等接口类型。基本的Supplier类型是个例外。
- 如果接口返回的是基本类型的结果,则会用To来表示,例如
ToLongFuncton<T>
和IntToLongFunction
。 - 如果接口返回的类型和参数类型相同,则会被命名为Operate,UnaryOperate用于表示一个参数,BinaryOperate用于表示两个参数。
- 如果接口接受的是一个参数并返回boolean,则会被命名为Predicate。
- 如果接口接受的是两个不同类型的参数,则名字中会有一个Bi(比如BiPredicate)。
要调用我们的方法,就要调用这个函数式方法的名字,而不是我们方法的名字。
13.4.1 带有更多参数的函数式接口
我们可以自己编写
@FunctionalInterface
public interface TriFunction<T,U,V,R>{
R apply(T t,U u,V v);
}
13.4.2 解决缺乏基本类型函数式接口的问题
直接编写Function<Integer,Double>
就能得到可行的方案,所以很明显,存在函数式接口的基本类型变种的唯一原因,就是为了放置在传递参数和返回结果时涉及自动装箱和自动拆箱,也就是说,为了性能。
似乎可以有把握的猜测,之所以有些函数式接口类型有定义,而有些没有,是根据预计的使用频率来决定的。
当然,如果因为缺少基本类型的函数式接口,导致性能真的出了问题,我们也可以很容易的编写自己的接口。
不过这成为性能瓶颈的可能性很小。
13.5 高阶函数
高阶函数就是一个可以接受函数作为参数或者能把函数当作返回值的函数。
知识点:
andThen()
会在调用之后调用,与之相对的,compose()
方法,会在之前应用编写的函数。
13.6 闭包
如果语言能够解决使用了外部变量的lambda表达式问题,那么我们称之为,这门语言是支持闭包的。
我们也可以称之为词法作用域,这里还涉及一个叫做变量捕获的术语。
在闭包的规则中:
凡是在lambda表达式或是类似语法的内部类中,使用了外部的变量的,就不能在return语句中修改字段的值。
例如return i++;//(i是外部类的字段的情况下)
。
就会引发编译错误。
自增也可以,但是自增的字段不可以出现在return上面。
下面就是一种解决方案。
public class Test{
IntSupplier makefun(int x){
int i = 0;
i++;
x++;
final int iFinal = i;
final int xFinal = x;
return () -> xFinal + iFinal;//在闭包使用x和i之前,先将其赋值给最终变量。
}
}
内部类作为闭包
结果证明,只要有内部类,就会有闭包。
13.7 函数组合
组合方法:
andThen(argument)
先执行原始操作,再执行参数操作。compose(argument)
先执行参数操作,再执行原始操作。and(argument)
对原始谓词和参数谓词执行短路逻辑与 (AND) 计算。or(argument)
对原始谓词和参数谓词执行短路逻辑或 (OR) 计算。negate()
所得谓词为该谓词的逻辑取反。
package fun.utree;
import java.util.function.Function;
public class FunctionComposition {
static Function<String, String> f1 = s -> {
System.out.println(s);
return s.replace("A", "_");
}, f2 = s -> s.substring(3), f3 = s -> s.toLowerCase();
public static void main(String[] args) {
System.out.println(f1.andThen(f3).compose(f2).apply("Go AFTER ALL AMBULANCES"));
}
}
13.8 柯里化和部分求值
package fun.utree;
import java.util.function.Function;
public class CurryingAndPartials {
static String uncurried(String a, String b) {
return a + b;
}
public static void main(String[] args) {
Function<String, Function<String, String>> sum = a -> b -> a + b;
System.out.println(uncurried("Hi ","Ho"));
Function<String,String> hi = sum.apply("Hi ");
System.out.println(hi.apply("Ho"));
//部分应用
Function<String,String> sumHi = sum.apply("Hup ");
System.out.println(sumHi.apply("Ho"));
System.out.println(sumHi.apply("Hey"));
}
}
到我写出这些字的时候,我还是明白这个概念的,希望以后还要继续复习这个概念。
14 流
集合优化了对象的储存,而流与对象的成批处理有关。
你会发现自己编程的重点将会从集合转向流。
流的一个重要的特征就是惰性求值,因为延迟求值,所以流是我们可以表示非常大的(甚至是无限大的)序列,而不用担心内存问题。
中间流操作distinct()
去掉重复的值。limit()
选择前几个值。元素是有序的sorted()
。
map()
:这个操作接受流中的每一个元素,在其中每个迭代过的元素进行一波操作后(这个操作可以自己定义)再顺着水流流到下面的操作里。
map()
有一些特殊的版本,比如mapToInt()
将一个对象流 转变成了一个包含Integers
的IntStream
,对于Float和Double,都有类似命名的操作。
为了从map集合中
获取流,我们首先调用entrySet()
来生成一个对象流。其中每个对象都包括着一个键和与其相关联的值,然后再 使用getKey()和getValue()将其分开。
split()
有着自己比较复杂的用法:在split()的参数里,可以这样写split("[ ?.,]+")
,后面的 + 表示前面的事物可以出现一次或多次。
generate()
它可以接受任何的Supplier
,并生成一个由 T 类型的对象组成的流。
java.util.stream.IntStream.*;
这里有一个方法叫做range(开始,结束)
,有两个参数,和python的range的功能是一样的。
Stream.iterate()
从一个种子开始(第一个参数),然后将其传给第二个参数所引用的方法。它运算的结果被添加到这个流上,并且保存下来作为下一次iterate()调用的第一个参数。
skip()
可以直接丢弃其参数指定的相应数目的流元素。
14.2 流的创建
14.2.5 流生成器
在生成器设计模式中,我们创建一个生成器对象,为他提供多段构造信息,最后执行“生成”动作。
Stream库提供了一个这样的Builder,它是一个内部类。
Stream.Builder<String> builder = new Stream.builder;
只要不调用build,就可以一直往流里面添加元素。如果调用之后依然添加,就会报异常。
14.2.6 Arrays
Arrays类中包含了名为stream()的静态方法,可以将数组转换成流。
14.2.7 正则表达式
在java.util.regex.Pattern
类中加入了一个新方法,splitAsStream()
。
它接受一个字符序列,并根据我们传入的公式将其分割为一个流。
这里有一个约束:splitAsStream()的输入应该是一个CharSequence,所以我们不能将一个流放进里面。
14.3 中间操作
peek()操作就是用来辅助调试的。它允许我们查看流对象而不修改他们。
因为peek()接受的是一个遵循Consumer函数式接口的函数,这样的函数没有返回值,所以也就不可能用不同的对象替换掉流中的对象。
我们只能看看这些对象。
14.3.2 对流元素进行排序
我们见过用默认的比较方式使用sorted()
进行排序的情况。
还有一种用Comparator
当作参数的sorted()
形式。
可以传入一个lambda函数作为sorted()的参数,不过也有预先定义好的Comparator。
14.3.3 移除元素
distinct()
:它移除了流中的重复元素。与创建一个Set来消除重复的元素相比,使用distinct要省力得多。filter(Predicate)
:过滤操作只保留符合特定条件的元素,也就是传给参数结果为true的那些元素。
14.3.4 将函数应用于每个流元素
map(Function)
:将Function应用于输入流中的每个对象,结果作为输出流继续传递。mapToInt(ToIntFunction)
:同上,不过结果是放在一个IntStream中。mapToLong(ToLongFunction)
:同上,不过结果是放在一个LongStream中。mapToDouble(ToDoubleFunction)
:同上,不过结果是放在一个DoubleStream中。
也就是说,如果Function生成的结果类型是某种类型,必须使用相应的mapTo操作来代替。
14.3.5 在应用map()期间组合流
flatMap()会做两件事,接受生成流的参数,并将其应用于传入元素(就像map())所做的的那样,然后再将每个流“扁平化”处理,将其展开为元素。所以传出来的就是一个一个的元素了。
flatMap(Function)
:当Function生成的是一个流时使用。flatMapToInt(Function)
:当Function生成的是一个IntStream时使用。flatMapToLong(Function)
:当Function生成的是一个LongStream时使用。flatMapToDouble(Function)
:当Function生成的是一个DoubleStream时使用。
引入了concat() -->
IntStream.concat(一个流,另一个流)
,它会按照参数的顺序将两个流组合到一起。
每当我们想要新的流,都必须从头创建。因为它无法复用。
14.4 Optional类型
findFirst()
:返回包含第一个元素的Optional。如果这个流为空,则返回Optional.empty。
findAny()
:返回包含任何元素的Optional,如果这个流为空,则返回Optional.empty。
max() min()
:分别返回流中最大值或最小值的Optional。如果这个流为空,则返回Optional.empty。
reduce()
:其中一个版本,它并不以一个”identity“对象作为其第一个参数(在reduce()
的其他版本中,“identity”对象会成为默认结果,所以不会有结果为空的风险),它会将返回值包含在一个Optional中。
对于数值化的流,IntStream与LongStream与DoubleStream,average()
操作将其结果包在一个Optional中,以防流为空的情况。
判断有或没有的时候,optional就派上用场了。
14.4.1 便捷函数
ifPresent(Consumer)
:如果值存在,则用这个值来调用Consumer,否则什么都不做。
orElse(otherObject)
:如果值存在,则返回这个对象,否则返回otherObject。
orElseGet(Supplier)
:如果对象存在,则返回这个对象,否则返回使用Supplier函数创建的替代对象。
orElseThrow(Supplier)
:如果对象存在,则返回这个对象,否则抛出一个使用Supplier函数创建的异常。
14.4.2 创建Optional
empty()
:返回一个空的Optional。
of(value)
:如果已经知道这个value不是null,可以使用该方法将其包裹在一个Optional中。
ofNullable(value)
:如果不知道这个value是不是null,可以使用该方法。如果value为null,它会自动返回Optional.empty,否则会将这个value包在一个Optional中。
14.4.3 Optional对象上的操作
filter(Predicate)
:将Predicate应用于Optional的内容,并返回其结果。如果不匹配,则将其转换为Optional.empty()。如果它本身就是empty,则直接传回去了。
map(Function)
:如果Optional不为empty,则将Function应用于Optional中包括的对象,并返回结果。否则传回Optonal.empty。
flatMap(Function)
:和map()类似,但是所提供的映射函数会将结果包在Optional中,这样flatMap()最后就不会再做任何包装了。
数值化的Optional上并没有提供这些操作。
14.5 终结操作
这些操作接受一个流,并生成一个最终结果。他们不会再把任何东西发给某个后端的流。
因此,终结操作总是我们在一个管线内可以做的最后一件事。
14.5.1 将流转换为一个数组
toArray()
:将流元素转换到适当类型的数组中。toArray(generator)
:generator用于特定情况下分配自己的数组储存。
14.5.2 在每个流元素上应用某个终结操作
forEach(Consumer)
:这种用法我们已经看到过很多次了 —— 以System.out::println
作为Consumer的函数。forEachOrdered(Consumer)
:这个版本确保forEach对元素的操作顺序是原始的流的顺序。__他牵扯到了parallel()
多处理器函数了。
14.5.3 收集操作
collect(Collector)
:使用这个Collector将流元素累加到一个结果集合中。collect(Supplier,BiConsumer,BiConsumer)
:和上面类似,但是Supplier会创建一个新的结果集合,第一个BiConsumer是用来将下一个元素包含在结果中的函数,第二个BiConsumer用于将两个值组合起来。
14.5.4 组合所有的流元素
reduce(BinaryOperator)
:使用BinaryOperator来组合所有的流元素。因为这个流可能为空,所以返回的是一个Optional。reduce(identity,BinaryOperator)
:这个和上面的一样,但是将第一个参数作为默认值。如果这个流是空的,那么identity将成为结果。reduce(identity,BiFunction,BinaryOperator)
:这个更复杂(所以我们不会介绍),但是之所以把他列在这里,是因为他可能更高效。可以通过组合显式的map()
和reduce()
操作来更简单的表达这样的需求。
14.5.5 匹配
allMatch(Predicate)
:当使用所提供的Predicate来检测流中的元素时,如果每个元素都得到true,则返回true。在遇到第一个false时,会短路计算。也就是说,遇到第一个false之后,它不会继续计算。anyMatch(Predicate)
:当使用所提供的Predicate来检测流中的元素时,如果任何一个元素得到true,则返回true。在遇到第一个true时,会短路计算。noneMatch(Predicate)
:当使用所提供的Predicate来检测流中的元素时,如果没有元素得到true,则返回true。在遇到第一个true时,会短路计算。
14.5.6 选择一个元素
findFirst()
:返回一个包含流中第一个元素的Optional,如果流中没有元素,则返回Optional.empty。findAny()
:返回一个包含流中某个元素的Optional,如果流中没有元素,则返回Optional.empty。
14.5.7 获得流的相关信息
count()
:获得流中元素的数量。max(Comparator)
:通过Comparator确定这个流中的“最大”元素“。min(Comparator)
:通过Compatator确定这个流中的“最小“元素“。
获得数值化流的相关信息
average()
:就是通常的含义,获得平均值max() min()
:这些操作不需要一个Comparator,因为他们处理的是数值化流。sum()
:将这些数值累加起来。summaryStatistics()
:返回可能有用的摘要信息。不太清楚为什么Java库的设计者觉得需要这个,因为我们自己可以用直接方法获得所有这些数据。
15 异常
15.2 基本的异常
异常参数:所有标准异常类都有两个构造器:第一个是无参构造器;第二个接受一个String参数,用于在异常中防止相关信息。
异常只是另一种对象,装饰我们的异常类的功能,不过,到了用的时候,这些装饰可能是没用的。因为出现错误的时候人们没有心情去看这些花里胡哨的东西。
15.5 异常说明
除了从RuntimeException继承而来的异常,这样的异常可以从任何地方抛出而不需要异常说明。
在编译时被检查并强制实施的异常叫做检查型异常
15.6 捕捉任何异常
15.6.2 栈轨迹
printStackTrace()
提供的信息也可以使用getStackTrace()
直接访问。
这个方法会返回一个由栈轨迹元素组成的数组。每个元素表示一个栈帧。元素0是栈顶,而且是序列中最后一个方法调用。(这个Throwable
被创建和抛出的位置)数组中最后一个元素和栈底则是序列中第一个方法调用。
15.6.3 重新抛出异常
如果重新抛出当前的异常,仍将是原来的异常抛出点的信息。
要加入新的栈轨迹信息,可以调用fillInStackTrace()
,它会返回一个Throwable对象。这个对象是它通过将当前栈的信息塞到原来的异常对象中而创建的。类似这样throw (Exception)e.fillInStackTrace();
fillInStackTrace()
被调用的那一行,成了这个异常的新起点。
重新抛出一个与所捕获的异常不同的异常也是可以的。这样做会得到与使用fillInStackTrace()
类似的效果,关于这个异常的原始调用点的信息会丢失。剩下的是与新的throw有关的信息。
15.7 标准Java异常
特例:RuntimeException(及其子类)
因为它是“非检查型异常”
15.9.3 finally 缺陷:异常丢失
在finally子句中执行return会把任何被抛出的异常都压制下来
或者在finally子句中返回了一个其他的Exception。
15.10 异常的约束
文字太多了。。自己总结一下
- 当一个类继承了另一个类,但也实现了一个接口的时候,如果接口中的A方法和父类中的A方法名字和throws的Exception一样,那么以父类为准,而不去实现接口的方法throws。
- 当一个父类在自己构造器中实现了throws Exception时,子类也要实现相同的异常。
- 强迫子类的方法遵守基类方法的异常说明,当没有throws的时候,子类的重写方法不可以自己擅自添加throws。
- 即使基类方法抛出了异常,其子类版本也可以选择不抛出任何异常。这是合理的,因为不会破坏针对基类版本会抛出异常这种情况编写的代码。
- 最后一个值得注意的地方就是,如果强制转向父类的类型,编译器就会(正确的..每个都要一模一样的)强制我们捕捉基类声明会抛出的异常。
- 异常说明并不是方法类型的一部分——方法类型只包括方法名字和参数类型。因此不能依赖异常说明的不同来重载方法。
- 总结:换句话说,在继承与重写的过程中,“异常说明”可以缩小,但是不能扩大——这与类在继承过程中的规则恰恰相反。
15.11 构造器
如果文件根本就没有打开(被FileNotFoundException捕获时),那么将不用关闭。其他catch语句必须关闭文件(因为已经找到了File并且打开了它)。
在这个示例里,finally字句绝对不是适合调用close()来关闭文件的地方。这是因为如果将其放到这里,每次构造器完成时,它都会关闭文件。然而我们希望的是,在对象的使用生命周期里,这个文件都应该是打开的。
15.12 try-with-resources 语句
普通的try-catch语句是
try{
}catch(Exception e){
}
而try-with-resources是
try( )
{
}catch(Exception e){
}
括号里的内容被称之为资源说明头。
更重要的是,不管如何退出try块(无论是正常还是异常),都会执行与上一个示例中的finally子句等同的操作(就是一个开启,一个close)。但是不需要编写那么复杂讨厌的代码了。
它是如何工作的呢,在新版语句的定义子句中(括号里)创建的对象必须都实现了java.lang.AutoCloseable
接口。该接口只有一个方法,close()
。
在退出try块时会调用两个对象的close方法,而且会以与创建顺序相反的顺序关闭他们。这个顺序很重要。
请注意:SecondException的close()方法,没有被调用,这是因为如果构造器失败了,我们不能假定可以在这个对象上安全地执行任何操作,包括关闭它在内。
不在资源头中定义的量,很容易被编译器忘记清理。(如果使用这个语法的话)
15.12.2 新特性:try-with-resources中的实际上的最终变量
JDK9增加了在try之前定义这些变量的能力,只要它们被显式的声明为最终变量,或者是实际上的最终变量即可。
但是尽量避免使用这个特性。有点憨。
15.14.4 将“检查型异常” 转换为 “非检查型异常”
在捕捉到一个异常的时候,重新用RuntimeException抛出,就变成了非检查型异常了。
然后,再把RuntimeException.getCause()将原始的原因搞出来。就能再次的catch了。
15.16 小结
有很多库,不使用异常就无法使用。“我相信 ‘ 报告 ’ 功能是异常的基本价值所在。
16 代码校验
17 文件
17.1 文件和目录路径
17.1.1 选择Path的内容
Path p = Paths.get("PartsOfPaths.java").toAbsolutePath();
p.getName()
:获取name
p.endsWith()
:比较的是整个路径组件,而不是名字中的一个组件。
相对应的是p.startsWith()
还有p.getNameCount()
。
17.1.2 分析Path
Path p = Paths.get("PartsOfPaths.java").toAbsolutePath();
Files.exists(p);
Files.isDirectory();
Files.isExecutable();
Files.isReadable();
Files.isRegularFile(p);
Files.isWritable(p);
Files.notExists(p);
Files.isHidden(p);
Files.size(p);
Files.getFileStore(p);
Files.getLastModifiedTime(p);
Files.getOwner(p);
Files.probeContentType(p);
Files.isSymbolicLink(p);
Files.getPosixFilePermissions(p);
17.1.3 添加或删除路径片段
Paths.get()
.toAbsolutePath()
.normalize();
Paths.toRealPath();
Paths.getParents();
Paths.getFileName();
Paths.relativize(); //去除前面的基准路径 例如C:/ 之类的。
Paths.resolve(); //在后面添加路径片段。
17.2 目录
作者想说明的是:如果我们对一个已经存在的目录调用了createDirectory()
,则会引发异常。
Files.createFile(Path p);
Files.createDirectory();//这个方法只能创建单层目录。
Files.createDirectories();//这个是可以创建多层目录的。
Files.createTempDirectory();
Files.createTempFile();//创建一个临时文件
Files.createtempDirectory();//创建临时文件夹
newDirectoryStream
,的流里面只有直辖目录,没有子目录。
要获得包含整个目录树内容的流,请使用Files.walk()
.
17.3 文件系统
在这里,我们可以使用静态的FileSystem工具来获得”默认“的文件系统。
但也可以在一个Path对象上调用getFileSystem()
来获得创建这个路径对象的文件系统。
FileSystem fsys = FileSystem.getDefault();
fsys.getFileStores();//返回的是数组
fsys.getRootDirectory();
fsys.getSeparator();
fsys.getUserPrincipalLookupService();
fsys.isOpen();
fsys.isReadOnly();
fsys.provider();
fsys.supportedFileAttributeViews();
FileSystem
还可以生成一个WatchService
和一个PathMatch
。
17.4 监听Path
WatchService使我们能够设置一个进程,对某个目录中的变化做出反应。
WatchService watcher = FileSystems.getDefault().newWatchService();
test是Path对象;
test.register(watcher,ENTRY_DELETE);
一旦从FileSystem得到一个WatchService,我们就将他和由我们感兴趣的事件组成的可变参数列表一起注册给test这个Path。
感兴趣的事件可以从ENTRY_CREATE
ENTRY_DELETE
或者 ENTRY_MODIFY
中选择。
watcher.take()
将一切工作停止下来 等待监听事件发生 它会返回一个WatchKey 里面会有你想要的信息.
watcher.reset()
重置监听池
watcher.cancel()
关闭监听池
17.5 查找文件
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("这里面放参数");
放什么参数呢 例如这些
String s = "glob :**/*.{tmp,txt}";
**/
表示所有子目录
17.6 读写文件
Files.readAllLines();
Files.write();
Files.size();
Files.lines();//可以很方便的将一个文件变为由行组成的Stream。
如果把文件当作一个由行组成的输入流来处理,那么Files.lines()就非常有用。但是如果我们想在一个流中完成读取,处理和写入,那该怎么办呢?
public class Stream{
publi static void main(String[] args){
try(
Stream<String> input = Files.lines(Paths.get("StreamnAndOut.java"));
PrintWriter output = new PrintWriter("StreamnAndOut.txt");
){
input
.map(String::toUpperCase)
.forEachOrdered(output::println);
}catch(Exception e){
throw new RuntimeException(e);
}
}
}
18 字符串
18.1不可变的字符串
String对象是不可变的,你之前使用的都是重新生成了一个新的String对象,如果想在原来的String上面做些有趣的事情,需要用到
StringBuilder
它提供了丰富而全面的方法:
append()
insert()
replace()
substring()
delete()
也有与他相同的类似的类。名字叫做StringBuffer,他是线程安全的,因此成本也明显更高。
使用StringBuilder进行字符串操作会更快。
18.4 对字符串的操作
18.5 格式化输出
18.5.2 System.out.format()
和C语言的printf()一样的家人们。
18.5.3 Formatter类
当创建一个Formatter对象时,你可以将信息传递给构造器,来表明希望将结果输出到哪里。
比如你这样Formatter formatter= new Formatter(new StringBuilder());
formatter.format();的输出就跑到append里面去了。
如果是Formatter formatter= new Formatter(System.out);
就跑屏幕输出里面了。
18.5.5 Formatter 转换
d 整数类型(十进制)
c Unicode字符
b Boolean值
s 字符串
f 浮点数(十进制)
e 浮点数(科学计数法)
x 整数类型(十六进制)
h 哈希码(十六进制)
% 字面量“%”
18.5.6 String.format()
他是一个静态方法,参数与Formatter了类的format()方法相同,但返回一个String。
当只调用一次format()时,这个方法用起来就很方便。
18.6 新特性:文本块 ( Java15 )
String s = """
%s
%.2f
""".formatted(tmp1,tmp2);
//本质上和printf()还是一样的。后面的参数就是用来输出用的。
//这个方法也是新添加的。也可以用于普通字符串。
18.7 正则表达式
18.7.4 Pattern 和 Matcher
只需要导入java.unit.regex
包,然后使用静态方法Pattern.compile()
编译正则表达式即可。
Pattern pattern = Pattern.compile("\\w+");
Matcher matcher = pattern.matcher(tmp1);//tmp就是将要比较的对象。
Pattern还有一个自己的静态方法matches( String regex , CharSequence input );
它返回一个Boolean的值。
它会检查正则表达式与整个CharSequece类型的输入参数input是否匹配。
分组:
A ( B ( C ) ) D
分组0是ABCD
分组1是BC
分组2是C
Matcher对象提供了一些方法,可以获取与分组相关的信息。
public int groupCount()
返回该匹配器模式中的分组数目。分组0不包括在此计数中。
public String group()
返回前一次匹配操作(例如 find() )的第0个分组(即整个匹配)。
public String group(int i)
返回前一次匹配操作期间给定的分组。如果匹配成功,但指定的分组未能匹配输入字符串的任何部分,则返回null。
public int start(int group)
返回前一次匹配操作中找到的分组的起始索引。
public int end(int group)
返回前一次匹配操作中找到的分组的最后一个字符的索引加1的值。
重要的是:
matches()
仅在整个输入字符串都与正则表达式匹配时才会成功,而 lookingAt(
) 则仅在输入字符串开始部分匹配时才成功。
Pattern.compile()还有一个重载的版本
Pattern.compile(String regex,int flag)
其中flag来自于Pattern类中的常量。
日常有用的是以下几种:
Pattern.CASE_INSENSITIVE (?i)
:默认情况下,匹配仅在US-ASCII字符集中进行时才不区分大小写。这个标记允许模式匹配时不考虑大小写。可以通过指定UNICODE_CASE标记,并结合这个标记来在Unicode字符集里启用不区分大小写的匹配。Pattern.MULTILINE (?m)
:做多行模式下,表达式^
和$
分别匹配一行的开头和结尾。此外,^匹配输入字符串的开头,$
匹配输入字符串的结尾。默认情况下,这些表达式仅匹配整个输入字符串的开头和结尾。Pattern.COMMENTS (?x)
:在这种模式下,空白符被忽略,并且以#开头的嵌入注释也会被忽略,直到行尾。
UNIX的行模式也可以通过嵌入的标记表达式来启用。
18.7.5 split()
split()还提供了一种重载后的方式。可以限制拆分的次数。
Pattern.compile("!!").split(input,次数);
18.7.6 替换操作
replaceFirst(String replacement)
用参数replacement替换输入字符串的第一个匹配的部分。replaceAll(String replacement)
用草书replacement替换输入字符串的每个匹配的部分。appendReplacement(StringBuffer sbuf , String replacement)
执行逐步替换,并保存到sbuf中,而不是像replaceFirst()和replaceAll()
那样分别替换第一个匹配和全部匹配。
这是一个非常重要的方法,因为你可以调用其他方法来处理或生成replacement
,(上面的那两个只能放入固定的字符串)。
使用此方法,你能够以编程的方式进行分组,从而创建更强大的替换功能。- 在调用了一次或多次的
appendReplacement
方法后,可以再调用appendTail(StringBuffer sbuf)
方法,将输入字符串的剩余部分复制到sbuf。(如果不调用也可以,运行不会出错,他们两个也不是必须配套使用的,只是不调用的话,距离最后一次匹配成功的那个元素的后面的文字就无法被添加进去了)
18.7.7 reset()
使用reset()方法将现有的Matcher对象应用于新的字符序列。
Matcher s = Pattern.compile("\\W+").matcher(s);
s.reset();//这就是回到了起始位置
s.reset("SSSSS");//这就是重设了需要进行匹配的字符串。
没有任何参数的reset会将Matcher对象设置到当前序列的起始位置。
18.8.2 使用正则表达式扫描
package org.example;
import java.util.Scanner;
import java.util.regex.MatchResult;
public class ThreatAnalyzer {
static String threatData =
"58.27.82.161@08/10/2015\n" +
"58.27.82.101@08/10/2015\n" +
"58.2.82.121@08/10/2015\n" +
"5.27.8.162@08/10/2015\n" +
"58.2.82.161@08/10/2015\n";
public static void main(String[] args) {
Scanner scanner = new Scanner(threatData);
String pattern = "(\\d+[.]+\\d+[.]\\d+[.]+\\d+)@(\\d{2}/\\d{2}/\\d{4})";
while (scanner.hasNext(pattern)) {
scanner.next(pattern);
MatchResult match = scanner.match();
String ip = match.group(1);
String date = match.group(2);
System.out.format("Threat on %s from %s%n", date, ip);
}
}
}
19 反射
获得class文件的方法.
其中一种是Class.forName("Name of Class")
另一种是new Object().getClass()
或者Object.class
请注意 传递给forName的字符串参数必须是类的完全限定名称(从第一个包开始的顺序)
getName()
getSimpleName()
:不带包的名称
getCanonicalName()
:完全限定的名称
Class.getInterfaces()
方法返回了一个Class对象数组,他们表示你所感兴趣的这个class对象的所有接口.
你也可以使用getSuperclass()
来查询对象的直接基类.
Class的newInstance()
方法是实现"虚拟构造器"的一种途径. -->这是一种已经被废弃的方法
现在常用的方法为getConstructor().newInstance()
重点 :
事实上 初始化会尽可能的懒惰 仅使用.class
语法来获取对类的引用不会导致初始化
而Class.forName()
会 立即初始化类 以产生Class引用
interfaceof 有一个相当严格的限制 只能将其与命名类型进行比较,而不能与一个Class对象进行比较.
但是有一个方法可以解决这个问题 那就是 Class.isInstance()
是动态验证对象类型的方式.
令人欣慰的是 instanceof 和 isInstance 产生了完全相同的效果
Class.forName()
生成的结果在编译时是未知的,因此所有的方法签名信息都是在运行时提取的.
现在你发现了,反射提供了足够的支持,来创建一个在编译时完全未知的对象.
19.7 动态代理
代理是基本的设计模式之一,他是为了代替实际对象而插入的一个对象从而提供额外的或不同的操作
java的动态代理更进一步,
详细请看newProxyInstance()
Proxy.newProxyInstance(Interface.class.getClassLoader(),
new Class[]{ Interface.class },
new DynamicProxyHandle(real));//
现在来到迷惑解答环节,
之前一直迷惑的问题之一,就是那个实现了InvovationHandle的接口的之后实现的方法
return的东西到底是啥:
你想想,有些方法有返回值,有些没有,那么是不是就要考虑到有返回值的情况,把需要返回的值return一下。
反射可以打到每一个元素身上,除了对于final类型无用之外,其他的都可以操作。
20 泛型
20.9 通配符
20.9.1 编译器有多聪明
重点:
这里需要注意下,回头记得做一下笔记,我刚刚想明白
只要自己将他向上转型(这个转型指的是Fruit<? extends Fruit>
)
就不能再往里面传输放东西了,因为编译器无法理解你要传入的东西是否是符合要求的
所以干脆不让大家往里面传输了
但是可以让你往外面输出,因为里面的值,函数在转型前是已经确认的,可以放心使用。
不过 参数为Object的方法没有问题 ,因为他接受的参数不是T,所以不会变化,而是一个确认的值。
Fruit<? super Fruit>
这造成了可以往里面添加,而不能提出来,因为你查看的有可能是任何类型,上面的类型太多了,无法判断是哪个。但是可以最低知道你往里面的传入的是什么。因此可以往里面传,但不能往外面取。
20.11 自限定类型
public interface GenericAndReturnType<T extends GenericAndReturnType<T>> {
T get();
}
interface Getter extends GenericAndReturnType<Getter>{}
class Extends{
public static void main(String[] args) {
Getter getter = new Getter(){
@Override
public Getter get() {
return null;
}
};
Getter getter1 = getter.get();
// GenericAndReturnType getter2 = getter.get();
}
}
这章太难用语言形容了,真的做不了笔记,满满的全部都是技巧,没有一丝感情
20.17 Java 8中的辅助潜在类型机制
Java可以做到连名字都不需要一样就可以完全调用。
更加泛型的方法。
使用Supplier的泛型方法
public static <T,C extends Collection<T>> C create(Supplier<C> fatory,Supplier<T> gen,int n){
return Stream.generate(gen)
.limit(n)
.collect(factory,C::add,C::addAll);
}
同时注意还返回了传入的容器的具体类型,因此类型信息并未丢失。
我本来很疑惑为什么C::add
可以实现,我记得是有类型擦除存在的。
但是我忘记了一个东西。那就是<T,C extends Collection<T>>
,它限定了这个类型最低是一个Collection。
21 数组
数组的优点在于,创建之后只能保存特定类型的对象,这意味着你将得到编译时类型检查的机制保障。
使用数组都不会有更大的风险。数组可以保存基本类型,而Collection只能借助装箱机制了。现在数组唯一的优势就是效率高了,因为集合有更多的功能和更少的限制。
21.2 数组是一等对象
唯一的区别在于,对象数组保存的是引用,而基本类型数组保存的是基本类型的值。
数组的length含义是数组的最大容量,即数组对象的size,而不是数组数组中实际保存的元素数量。
不过一个数组对象被创建后,其中所有的元素的引用都会自动地初始化为null,因此便可以用判断null来确认这个位置是否存了数据。
另一个,基本类型的数组会自动地初始化为0;对于char类型,会初始化为(char)0
,对于boolean类型,则是自动的为false。
例如,假设hide()是一个将对象数组作为参数的方法,你可以这样调用它。
hide(a);
你也可以在传入参数的地方,直接动态的创建一个数组。
hide(new Belly[]{
new Belly(),
new Belly(),
new Belly(),
new Belly(),
});
以下表达式
a = b;
展示了如何将一个指向数组对象的引用,分配到另一个数组对象。(对任何类型对象引用的操作都是如此)
这样一来,a和b都指向了堆上的同一个数组对象。
其他的唯一区别就是,对象数组只是保存了对象的引用,而基本类型真真正正的保存了基本类型的值。
21.3 返回数组
不同于C语言和C++只能返回数组的首地址,Java可以返回一整个数组。
而且,本书推荐使用的随机数不再使用Random,而是开始推荐SplittableRandom
不仅支持并行操作(迟早要接触到的)而且还能生成更高质量的随机数。
21.4 多维数组
Arrays.deepToString(a)
将多维数组转化为String。
数组中构成矩阵的每组向量的大小没有限制【称为不规则数组】;
21.5 数组和泛型
数组本身的类型是可以参数化的。
class ClassParameter<T> {
public T[] f(T[] arg){
return arg;
}
class MethodParameter {
public static <T> T[] f(T[] arg){
return arg;
}
}
}
21.6 Arrays.fill()
可以将一个数值复制到数组的所有位置。
Arrays.fill(a9,"hello");
Arrays.fill(a9,2,4,"hello");
21.12 关于数组并行
我们可以用setAll()
来初始化更大的数组,如果速度成了问题,Arrays.parallelSetAll()
,应该能更快的完成初始化。
21.14 数组复制
Arrays.copyOf(a1,a1.length);
Arrays.copyOfRange(a3,4,12);//从第四到第十二的副本。
基本类型数组和对象数组都可以进行复制。然而,如果要复制对象数组,则只有引用会被复制,这是因为并不存在对象自身的副本。这被称为浅拷贝。
此外还有一个方法,System.arraycopy()
,也可以将一个数组复制到另一个已经分配好的数组中。
此处不会发生自动装箱或是自动拆箱,这是因为相关的两个数组的类型必须是完全一致的。
21.15 数组比较
Arrays
类提供了equals()
方法来比较一维数组是否相等,以及deepEquals()
方法来比较多维数组。
这些方法都有针对所有基本类型和对象类型的重载版本实现。
两个数组相等意味着其中的元素数量必须相等。
并且每个相同位置的元素也必须相等。
21.16 流和数组
Arrays.stream()
可以很方便的根据某些类型的数组生成流。
数组排序
Comparable接口:用在类里,只有实现了这个接口,才能使用sort方法。
Comparator接口:用在sort的第二个参数里面,作为排序的方案。对原类型的改动为零。
Arrays.sort():通过内建的排序方法,可以对任何基本类型或者对象类型(实现了Comparable接口或实现了对应的Comparator)进行排序。
并行排序
Arrays.parallelSort()
:并行排序
Arrays.binarySearch()
:进行二分查找
使用binarySearch方法检索(二分法)的前提是要对集合内数据进行排序,否则返回的值是不准确的!
用Arrays.parallelPrefix()
进行累计计算
本文来自博客园,作者:Y&Qter,转载请注明原文链接:https://www.cnblogs.com/qiantaosama/p/16712889.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?