大家好,又见面了,我是你们的朋友全栈君。
Java核心技术(卷1)
一、基础概念
1.1 基本程序设计结构
- 1.1 数据类型
- 1.1.1 数值类型
- 1️⃣从java7开始,加上前缀0b或0B就可以写二进制;
- 2️⃣指数的表示
- 十进制中以10为底指数的表示:
- double d = 1.0e 4; // 10000.0
- double d2 = 100000.0e-4; // 10.0
- 十六进制中以2位底指数的表示:
- double a1 = 0x1.0p 3; // 8.0
- double a2 = 0x1.0p-3; // 0.125
- 十进制中以10为底指数的表示:
- 3️⃣double中的严格浮点计算:strictfp
- double类型使用64位存储一个数值,而有些处理器使用80位浮点寄存器。这些寄存器增加了中间过程的计算精度(将中间结果存储在80位寄存器中,将最终结果截断位64位)。这个过程增加了计算精度,但结果可能与始终在64位机器上计算的结果不一样。
- 如果将一个类标记为strictfp,那么这个类的所有方法都要使用严格的浮点计算。
- 严格浮点计算:所有的中间计算必须进行截断。
- 默认情况下是不采用严格的浮点计算。
- double类型的三个特殊值:增无穷大、负无穷大、NaN
- Double.POSITIVE_INFINITY // 正无穷大
- Double.NEGATIVE_INFINITY // 负无穷大
- Double.NaN
- 检查一个字符是否为数值:Double.isNaN(x)
- 1.1.2 Unicode和char类型
- Unicode
- 专业术语
- 码点:某个字符所对应的代码值;
- Unicode中码点的表示:Unicode的码点可以分为17个级别,第一个级别,码点从U 0000到U FFFF,被称为基本的多语言级别,其中包括经典的Unicode代码;其余的16个级别的码点从U 10000到U 10FFFF,其中包括一些辅助字符。
- 代码单元:用8位或16位(UTF-8是8位表示,UTF-16用16位表示)字节来表示Unicode中基本的多语言级别,被称之为代码单元。也就是说,UTF-8中代码单元是8位字节,UTF-16中代码单元是16位字节。
- UTF-16编码采用不同长度的编码表示所有的Unicode码点
- 在UTF-16中,表示一个Unicode码点,需要一个或两个代码单元。
- 专业术语
- char类型
- 在java中,char类型描述了UTF-16的一个代码单元
- 因此,“char用来表示单个字符”的说法不完全正确。
- char类型是用16位表示
- 有些unicode字符可以用一个char值来描述,另一些Unicode字符需要用两个char值来描述。
- 在java中,char类型描述了UTF-16的一个代码单元
- 区分码点、代码单元的用途:
- length() 方法
- length方法将返回UTF-16编码表示的给定字符串所需要的代码单元数量。
- 想要获取实际长度,即码点数量,即字符个数
- String greeting = “hello”; int cpCount = greeting.codePointCount(0, greeting.length());
- 获取第几个代码单元:
- String greeting = “hello”; char first = greeting.atChar(0);
- 获取第几个码点:
- String greeting = “hello”; // i 为第i个码点,i从0开始 int index = greeting.offsetByCodePoints(0, i); greeting.codePointAt(index);
- length() 方法
- Unicode
- 1.1.3 大数值: BigInteger和BigDecimal
- BigInteger可以实现任意精度的整数运算; BigDecimal可以实现任意精度的浮点运算;
- 1.1.1 数值类型
- 1.2 规范
- 1.2.1 变量
- 关于“$”
- 尽管$是一个合法的Java字符,但不要在自己的编码中使用这个字符,它只用在Java编译器或其他工具生成的名字中。
- 关于“ ”的使用
- 建议不要在代码中使用“ ”,这样的代码很容易让人困惑,而且会带来烦人的bug。
- 关于“$”
- 1.2.1 变量
- 1.3 运算符
- 1.3.1 位运算符
- 和<<运算符将位模式左移或右移
- 运算符会用0填充高位;>> 会用符号位填充高位;不存在<<< 运算符;
- 位运算符的右操作数要完成模32的运算(除非左操作数是long类型,这种情况下需要对右操作数模64):1<<35 等于1<<3 等于8
- 1.3.2 运算符的级别
- &&的优先级比|| 的优先级高
- = 是右结合运算符,所以表达式: a =b =c 等价于: a =(b =c)
- 1.3.1 位运算符
- 1.4 String
- 1.1 String是不可变字符串。一定不要使用“==”检测两个字符串是否相等,每次连接字符串,都会构建一个新的String对象;
每次连接都会创建一个新的字符串,这样既耗时,又浪费空间。使用StringBuilder类就可以避免这个问题的发生。
StringBuilder的前身是StringBuffer。StringBuffer效率稍微有些低,但允许采用多线程的方式执行添加或删除字符的操作。
StringBuilder 除了能够apped字符串,还能添加代码单元,添加码点
所有字符串都在一个单线程中编辑,则应该用StringBuilder代替它。
- 编译器可以让字符串共享。如果虚拟机使用让字符串共享就可以使用“==”运算符检测两个字符串是否相等。但,实际上只是字符串常量可以共享,而 或substring等操作产生的结果并不共享。
- “==”运算符能够用来确定两个字符串是否放在同一个位置。如果两个字符串放在同一个位置,它们必然相等。完全有可能将相同内容的字符串放在不同的位置上。
- 使用equals 方法比较两个字符串是否相等。
- 1.2 String 的length方法将返回采用UTF-16编码表示的给定字符所需要的代码单元数量
- 要想获取实际的字符长度,即码点数量,可以调用:codePointCount 方法
- public static void test04() throws UnsupportedEncodingException {
String greeting = “Hello”;
String codeStr = “uD835uDD46”;
byte[] b = codeStr.getBytes(“UTF-16”);
String s = new String(b, “UTF-16”);
System.out.println(“greeting:” greeting);
System.out.println(“一个UTF-16编码的特殊符号s:” s);
System.out.println(“greeting.length() : ” greeting.length()); // 打印出 5
System.out.println(“greeting.codePointCount() : ” greeting.codePointCount(0, greeting.length())); // 打印出5
System.out.println(“s.length() : ” s.length()); // 打印出 2
System.out.println(“s.codePointCount() : ” s.codePointCount(0, s.length())); // 打印出 1
}
- greeting:Hello 一个UTF-16编码的特殊符号s:? greeting.length() : 5 greeting.codePointCount() : 5 s.length() : 2 s.codePointCount() : 1
- 1.3 连接字符串:join 方法,可以指定分界符
- 1.4 所有字符串都属于CharSequence接口
- 1.1 String是不可变字符串。一定不要使用“==”检测两个字符串是否相等,每次连接字符串,都会构建一个新的String对象;
每次连接都会创建一个新的字符串,这样既耗时,又浪费空间。使用StringBuilder类就可以避免这个问题的发生。
StringBuilder的前身是StringBuffer。StringBuffer效率稍微有些低,但允许采用多线程的方式执行添加或删除字符的操作。
StringBuilder 除了能够apped字符串,还能添加代码单元,添加码点
所有字符串都在一个单线程中编辑,则应该用StringBuilder代替它。
- 1.5 控制流程
- “break 和 跳出标签”的用法:可以将标签应用到任何语句中,甚至可以应用到if语句或块语句中
- 1.6 数组
1.2 对象和类
- 类
- 在一个源文件中,只能有一个共有类,但可以有任意数目的非公有类。
- 构造器
- 构造器与类同名
- finalize方法
- 可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清楚对象之前调用(但是很难知道这个方法什么时候才能被调用)。
- 对象
- Java的对象都是在堆中创造;
- ⚠️警告:不要编写返回引用可变对象的访问器方法。如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。
- // 错误代码示例 /**
- Date对象有一个更改器方法setTime。
- 也就是说Date对象是可变的,这一点就破坏了
- 封装性!
*/
public Date gethireDay(){
return this.hireDay;
}
– // 正确的做法
public Date getHireDay(){
return (Date)this.hireDay.clone();
}
- 封装:
- 封装通常提供三项内容:
- 一个私有的数据域;
- 一个公有的域访问器方法;
- 一个公有的域更改器方法;
- 封装的好处:
- 1️⃣可以改变内部实现;
- 2️⃣更改器方法可以执行错误检查;
- 警告:永远不要编写返回引用 可变对象 的方法器方法。
- 如果要返回一个可变对象的引用,首先对其进行克隆,否则将破坏封装性;
- 例如: LocalDate类没有更改器方法; Date对象是可变的
- 封装通常提供三项内容:
- 调用构造器的具体处理步骤:
- 0、静态域在类被加载的时候进行初始化。静态代码块在类第一次的时候执行一次。(创建对象的时候不再执行)
- 1、所有数据域被初始化为默认值(0、false、或null);
- 2、按照在类声明中出现的顺序,依次执行所有域初始化语句和初始化块;
- 对于类的静态域进行初始化的代码比较复杂,可以使用静态的初始化块;
- 3、如果构造器第一行调用了第二个构造器,则执行第二个构造器主体;
- 4、执行这个构造器的主体;
- 封装:
- 修饰符:
- final 实例域
- 可以将实例域声明为final,在构造对象的时候必出初始化这样的域;
- final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域。对于可变的类,使用final修饰可能会对读着造成混乱
- final 修饰的类不允许被继承;final修饰的方法不允许被覆盖;
- 如果将一个类声明为final,只有其中的方法自动地成为final,而不包括域;
- static 方法
- 建议使用类名而不是对象名来调用静态方法,因为静态方法和对象没有任何关系。
- 静态方法的另一种常见用途是使用静态工厂方法来构造对象。
- final 实例域
- 静态导入(import)
- import语句不仅可以导入类,还增加了导入静态方法和静态域的功能
- 其他
- 编译运行带有包的源文件
- 编译器在编译源文件的时候不检查目录结构,如果它不依赖其他包就不会出现编译错误。但如果编译后的类文件没有在声明的包下,程序最终无法运行。
- 类路径
- 类文件可以存储在JAR文件中。使用jar文件需要设置classpath
- 文档注释:javadoc -d docDirectory nameofPackage1 nameofPackage2 ……
- 编译运行带有包的源文件
- Java用于控制可见性的4个修饰符:
- private 仅对本类可见;
- public 对所有类可见;
- protected 对本包和所有子类可见;
- 如果子类和父类不再同一个包下,应当用子类对象访问父类protected方法;
- 默认,不需要修饰符 对本包可见;
- 类的加载:
- 在启动时,包含main方法的类被加载。它会加载所需要的类。这些被加载的类又要加载它们需要的类,依次类推。 对于一个大型的应用,可以使用技巧给用户一个启动速度比较快的幻觉: 1、确保包含main方法的类没有显式地引用其他类; 2、显示一个启动画面; 3、通过调用Class.forName手工加载其他的类;
1.3 继承
- 继承是Java的核心技术
- super和this
- 有些人认为super与this引用是类似的概念,实际上这样的比较并不恰当。因为super并不是一个对象的引用,不能讲super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
- this的用途
- 1、引用隐式参数;
- 2、调用该类的其他的构造器;
- super的用途
- 1、调用超类的方法;
- 2、调用超类的构造器;
- 子类和超类
- 子类
- 子类构造器
- 使用super调用构造器的语句必须是子类构造器的第一条语句;
- 如果子类构造器没有显式调用超类构造器,将自动地调用该超类的默认构造器(没有参数的构造器)
- 如果子类构造器没有显式的调用超类构造器,超类又没有不带参数的构造器,Java编译器将报告错误
- 子类构造器
- 类型转化
- 只能在继承层次内进行类型转化;
- 在将超类转化为子类之前,应该使用instanceof进行检查;
- 只有在使用子类特有方法的时候才需要类型转换
- 一般情况下,应该尽量少用类型转换和instanceof运算符
- 子类
- 多态和动态绑定
- 抽象类
- 抽象类不能被实例化
- 多态:
- 一个引用类型的变量既可以引用当前类型的对象,也可以用子类型的对象;
- 动态绑定
- 理解方法的调用
- 抽象类
- 覆盖
- 方法名字和参数列表称为方法的签名;
- 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。称之为“协变返回类型”
- Object
- 编写一个完美的equals方法
- 设计原则,equals方法要满足五个特性:
- 1、自反性;
- 2、对称性;
- 3、传递性;
- 4、一致性;
- 5、对于任意非空引用x,x.equals(null)方法应该返回false;
- 设计思路:
- 1、如果子类能够拥有自己的相等概念,则对称性要求将强制采用getClass进行检测;
- 2、如果超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。并应该将超类的equal方法声明为final;
- 完美的equals方法的建议:
- 1、显示参数命名为otherObject,稍后需要将它转换成另一个叫other的变量;
- 2、检测this域otherObject是否引用同一个对象:if (this == otherObject) return true;
- 这个比较只是一个优化;
- 3、检测otherObject 是否为null,如果为null,返回false: if (otherObjct == null) return false;
- 这个检测很有必要
- 4、比较 this与otherObject 是否属于同一个类,如果equals的语义在每个子类中有所改变,就是用getClass检!= otherObject.getClass()) return false;
- 设计原则,equals方法要满足五个特性:
- 编写一个完美的equals方法
4、如果所有的子类都有统一的语义,就使用instanceof检测: if (!(instanceof instanceof ClassName)) return false;
代码语言:javascript复制 - Java为每个类型管理了一个Class对象。因此,可以利用 == 运算符实现两个类对象比较的操作。
- 5、将otherObject转化为相应的类型变量:
ClassName other = (ClassName) otherName; – 6、现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。所有的域都匹配返回true,否则返回false; – 7、如果子类中重新定义equals,就要在其中包含调用super.equals(other)
代码语言:javascript复制 - 如果重新定义equals方法,就必须重新定义hashCode方法
- toString方法
- 只要对象与一个字符串通过操作符“ ”连接起来,Java编译器就会自动调用toString方法
- 在调用x.toString() 的地方可以用"" x替代
- println 方法就会直接调用x.toString() 并打印输出得到的字符串
- 强烈建议为自定义的每一个类增加toString方法
- 泛型数组列表ArrayList
- ensureCapacity 方法
- 这个方法为数组列表分配一个初始容器,这样向数组列表中添加元素时,添加元素数量不大于初始化容器大小时,数组列表不用重新分配空间
- trimToSize 方法
- 这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
- ensureCapacity 方法
1.4 对象包装器和自动装箱
- 对象包装器是不可变的
- 装箱和拆箱是编译器认可的,而不是虚拟机
1.5 枚举类
- 在比较两个枚举类型的值时,永远不需要调用equals,而直接使用“==”就可以了
- 所有的枚举类型都是Enum类的子类
- 最有用的方法
- toString() 这个方法能够返回枚举常量名
- Size.SMALL.toString() 将返回字符串“SMALL”
- valueOf() 这个方法是toString的逆方法
- Size s = Enum.valueOf(Size.class, “SMALL”); 将s 的值设置为 Size.SMALL
- values() 这个方法返回一个包含全部枚举值的数组
- ordinal() 这个方法返回enum声明中枚举常量的位置,位置从0开始计数。
- toString() 这个方法能够返回枚举常量名
二、Java的高级特性
2.1 反射
- 能够分析类能力的的程序称为反射
- 在运行时分析类的能力
- 在运行时查看对象
- 2.1.1 Class类
- 一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如:int不是类,但int.class 是一个class类型的对象
- Java为每个类型管理了一个Class对象。因此,可以利用 == 运算符实现两个类对象比较的操作。
- if(e.getClass() == Employee.class)
- 创建一个类的实例
- e.getClass().newInstance();
- String s = “java.util.Random”; Object n = Class.forName(s).newInstance();
- 利用反射分析类的能力
- Class类中的getFields、getMethods、getConstructors 方法将分别返回类提供的public域、方法和构造器数组
- Class类中的getDeclareFields、getDeclareMethods、getDeclareConstructors 方法将返回类中声明的全部域、方法和构造器
- 实战:构建一个通用的toString 方法
2.2 接口
- 基础概念:
- 接口的所有方法自动地属于public
- 接口中不能包含实例域或静态方法,但却可以包含常量。接口中的域将被自动设为public static final;
- 接口中的方法标记为public,接口中的域将被自动设为public static final。 Java语言规范建议不要书写这些多余的关键字。
- 每个类只能拥有一个超类,但却可以实现多个接口;
- 使用instanceof 检查一个对象是否属于某个特定类,也可以使用使用instanceof 检查一个对象是否实现了某个特定的接口
- 使用逗号将实现(implement) 的各个接口分隔开;
- 抽象类和接口
- 抽象类表示通用属性,有一个问题;每个类只能扩展于一个类;
- 接口可以提供多重继承的大多数好处,同时还能避免继承的复杂性和低效性。
- 新认识:
- 静态方法
public interface Path{
public static Path get(String first, String …more){
return FileSystem.getDefault().getPath(first,more);
}
}
- 在Java8中,允许在接口中增加静态方法。
- 通常是将静态方法放到伴随类中。在标准库中,可以看到成对出现的接口和实用工具类,如: Collection/Collections
- 以后在实现自己的接口时,不再需要为实用工具方法另外提供一个伴随类。
- 默认方法
public interface Comparable {
default int compareTo(T other){return 0;}
}
- 可以为接口方法提供一个默认实现。必须使用default修饰符标记这样一个方法。
- 默认方法可以调用任何其他方法
- 默认方法的一个重要用途是“接口演化”
- 为旧的接口类添加一个新的默认方法,这样可以兼容旧的代码;
- 解决默认方法的冲突:
- 1)超类优先; 如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略;
- 子主题 3
- 2)接口冲突; 如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否为默认方法)相同的方法,必须覆盖这个方法来解决冲突;
- 静态方法
public interface Path{
public static Path get(String first, String …more){
return FileSystem.getDefault().getPath(first,more);
}
}
- 标记接口:
- 标记接口唯一的作用就是允许在类型检查中使用 instanceof
- 建议自己的程序中不要使用标记接口
- 函数式接口:只有一个抽象方法的接口
- 常用的函数式接口
- 基本的函数式接口
2.3 lambda表示式、方法引用、构造器引用
- 概要:
- lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。
- 函数式接口:只有一个抽象方法的接口
- 在java中,lambda表达式所能做的也只是能转换为函数式接口。
- 想要用lambda表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口。
- ArrayList类有一个removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。
- // 删除数组列表中为null的值 list.removeIf(e->e==null);
- lambda 表达式的形式及组成:
- lambda 表达式形式:参数,箭头(->)以及一个表达式
- lambda有三部分构成:
- 1)一个代码块;
- 2)参数;
- 3)自由变量的值,这里指非参数而且不在代码中定义的变量;
- 代码块以及自由变量有一个术语,叫:闭包
- lambda就是Java的闭包
- lambda表达式可以捕获外围作用域中变量的值。但这里有一条规则:lambda表达式中捕获的变量必须实际上是最终变量。实际变量是指,这个变量初始化之后就不会再为它赋新值。
- lambda表达式中引用的变量值,既不能在lambda表达式中改变,也不应该在外部改变。
- lambda中不能有同名的局部变量;
- lambda 表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
- 例如: public class Application(){ public void init(){ ActionListener listener = event -> { System.out.println(this.toString()); } } }
这里的this指的是ActionListener
- lambda 表达式补充:
- lamdba表示式的返回类型总是由上下文推导得出;
- 需要函数式接口的时候,可以提供一个lambda表达式代替;
- 最好把lambda表示式看做函数,而不是一个对象。lambda表达式可以转换为接口
- 想要用lambda表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口。
- lambda表达式的重点是延迟执行
- 方法引用
有些时候已经有现成的方法可以完成你想要传递到其他代码的某个动作。
- 方法引用,主要有三种情况:
- object::instanceMethod
- 如: System.out::println 等价于 x -> System.out.println(x)
- Class::staticMethod
- 如: Math::pow 等价于 (x,y) -> Math.pow(x,y)
- Class::instanceMethod
- 第一个方法会成为方法的目标
- 如: String::compareToIgnoreCase 等价于 (x,y) -> x.compareToIgnoreCase(y)
- object::instanceMethod
- 方法引用中可以使用this和super
- 如: this::equals 等价于 x->this.equals(x)
- super::greet
- 方法引用,主要有三种情况:
- 构造器引用
- 例如:Person::new 是Person构造器的一个引用
- int[]::new 等价于 x->new int[x]
2.4 内部类
- 为什么使用内部类:
- 内部类方法可以访问该类定义所有的作用域中的数据,包括私有的数据;
- 内部类可以对同一个包中的其他类隐藏起来
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷
- 内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
- 只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
- 内部类中所有的静态方法域都必须是final
- 内部类不能有static方法
- 只有内部类可以声明为static
- 在内部类不需要访问外围类对象的时候,应该使用静态内部类。
- 内部类的种类
- 局部内部类
- 如果一个内部类只在被一个方法访问,就可以将这个内部类定义到方法中。
- 局部类有一个优点:它们不仅能够访问包含它们的外部类,还可以访问局部变量;在JavaSE8 之前,局部变量声明为final;
- 匿名内部类
- 如果定义在方法中的内部类只创建一个对象,就可以使用匿名内部类替代。
- 尽可能使用lambda表达式代替匿名内部类
- 静态内部类
- 有时候,使用内部类只是把一个类隐藏在另一个类的内部,并不需要内部类引用外围类对象。 为此,可以将内部类声明为static,以便取消产生的引用。
- 局部内部类
2.5 代理
- java的JDK里面提供了动态代理的方案
2.6 Java的异常
- 异常的层次结构:
- 所有异常都是由Throwable继承而来, 而下一层立即分为两类:Error和Exception
- Exception层又分解为两支:一支派生于RuntimeException,另一支包含其他异常
- 由程序错误导致的异常属于RuntimeException
- 如果RuntimeException出现,一定是你的问题
- 而程序本身没问题,但由于像I/O错误这类问题导致的异常属于其他异常
- 由程序错误导致的异常属于RuntimeException
- 受查异常和非受查异常
- 派生于Error类或RuntimeException类的所有异常称为非受查异常;
- 所有其他异常称为受查异常
- 策略:一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控(Error),要么就应该避免发生。
- 合并catch语句
- 形式: catch(XXXException | XXXException e)
- … catch (IOException|InterruptedException|ClassNotFoundException e) { e.printStackTrace(); throw new RuntimeException(e); }
- 只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
- 捕获多个异常时,异常变量隐含为final变量,不能再子句体中为e赋不同的值。
- 形式: catch(XXXException | XXXException e)
- 再次抛出异常,可以将原始异常设置为新异常的原因
- 例如: Throwable se = new ServletException(“database error”); se.initCase(e); throw se;
- 重新得到原始异常:Throwable e = se.getCase();
- 强烈建议将try/catch和try/finally语句块解耦,这样可以提高代码的清晰度。
- try{ try{ code that might throw exceptions }finally{ in.close(); } }catch(IOException e){ show error message }
- 带资源的try语句
- 最简单的形式如: try (Resource res = …) { work with res }
- 还可以指定多个资源: try (Scanner in = new Scanner(new FileInputStream(“/user/words”),“UTF-8”); PrintWriter out = new PrintWriter(“out.txt”)){
}
代码语言:javascript复制 - 无论这个块如何退出,in和out都会关闭
2.7 泛型程序设计
- 2.7.1 泛型类
- public class Pair
- 2.7.2 泛型方法
- 例如: public static getMiddle<T… a>
- 类型变量放在修饰符后面,返回类型的前面;
- 2.7.2 类型变量的限定
- 例如:
public static T min(T[] a)…
- 将T限定为实现了Comparable接口的类
- 一个类型变量使用通配符可以有多个限定: T extends Comparable & Serializable
- 为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾
- 例如:
public static T min(T[] a)…
- 2.7.3 类型擦除
- 虚拟机没有泛型类型对象。
- 无论何时定义一个泛型类型,都自动提供了一个响应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定的变量用Object)
- 为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾
- 2.7.4 通配符类型
- 例如:
Pain<? extends Employee>
- 表示任何泛型Pain类型
- 通配符限定与类型限定十分类似
- 但,通配符限定有一个附加能力,可以指定一个超类型限定,如:? super Manager ,这个通配符限制为Manager的所有超类型
- 例如:
Pain<? extends Employee>
- 2.7.5 无限定通配符
- class Pair<?>{
? getFirst()
void setFirst(?)
}
- getFirst() 的返回值只赋给一个Object。setFirst方法不能被调用,甚至不能用Object调用。可以调用 setFirst(null)
- 用来测试一个pair是否包含一个null引用
- 非法的代码:? t = p.getFirst();// Error
- class Pair<?>{
? getFirst()
void setFirst(?)
}
2.6 应用首选项的存储
三、图形程序设计
3.1 Swing
- 解释:“被绘制的”用户界面类。
3.2 AWT
- 解释:像事件处理这样的窗口工具箱的底层机制。
四、部署Java应用程序
4.1 Jar文件
- 4.1.1 创建Jar文件;
- 4.1.2 清单文件;
- 4.1.3 可执行Jar文件;
- java -jar jar文件
4.2 应用首选项的存储
- 4.2.1 属性映射
- Properties
- 4.2.2 首选项API
五、集合
集合框架的接口
- Iterable
接口
- Collection
Collection 是接口。
Collection实现Iterable的接口,这样就能够使用for each 语法了
- List
- Set
- Queue
- Collection
Collection 是接口。
Collection实现Iterable的接口,这样就能够使用for each 语法了
- Map
- SortMap
- NavigableMap
- SortMap
- Iterator
- ListIterator
集合框架中的类
- AbstractCollection
- AbstractList
- AbstractSequentialList
- LinkedList
- ArrayList 数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价。在数组中间的位置上插入一个元素也是如此。 链表(Linked list) 解决了这个问题。
- AbstractSequentialList
- AbstractSet
- HashSet
- LinkedHashSet
- EnumSet
- TreeSet
- HashSet
- AbstractQueue
- PriorityQueue
- ArrayQueue
- AbstractList
- AbstractMap
- HashMap
- LinkedHashMap
- TreeMap
- EnumMap
- WeakHashMap
- IdentityHashMap
- HashMap
队列
- 双端队列
- 优先级队列
- 使用优先级队列的典型示例是任务调度
链表
- 在Java程序设计语言中,所有链表实际上都是双向连接的。
- LinkedList对象根本不做任何缓存位置信息的操作。每次查找一个元素都要从列表的头部重新开始搜索。
- 使用链表的唯一理由是尽可能减少在列表中间插入或删除元素所付出的代价。
数组列表
散列集
- 散列表可以快速查找所需要的对象。
- 原理:在Java中,散列表用链表数组实现。每个列表称为桶。要想查到表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得的结果就是保存这个元素的桶的索引。
- 散列码:hashCode
- Java8中,桶满时会从链表变为平衡二叉树。
- 初始化桶数
- 散列因子
- HashSet类,实现了基于散列表的值。
树集
- TreeSet
- 树集是一个有序集合
- 排序是用树结构完成的
映射
- 用迭代处理映射的键和值
- scores.forEach((k,v) -> System.out.println(“key=” k “,value” v));
- HashMap和TreeMap
两个通用的实现。
- HashMap,对键进行散列
- TreeMap,用键的整体顺序对元素进行排序,并将其组织成搜索树。
- 要迭代处理映射的键和值,最容易的方法是使用forEach方法。可以提供一个接收键和值的lamdba表达式。
- scores.forEach((k,v)->System.out.println(“key=” k ”,value=” v));
- LinkedHashMap
- 链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。
- 每次调用get或put,受到影响的条目将从当前位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。
- 链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。
- WeekHashMap
- 这个数据结构将与垃圾回收期协同工作,用于回收不再有任何途径引用的键值
- IdentityHashMap
- 键的散列值不使用hashCode函数计算的,而是用System.identityHashCode方法计算。而且对两个对象进行比较时,IdentityHashMap类使用==,而不使用equals。
视图与包装器
- 映射类keySet() 返回一个实现了Set接口的类对象,这个类的方法对原映射进行操作。这种结合称为视图。
- Arrays类的静态方法asList将返回一个包装了普通Java数组的List包装器。 Card[] cardDeck = new Card[52]; List cardList = Arrays.asList(cardDeck); 返回的对象不是ArrayList。它是一个视图对象。
- 同步视图
- Collections.synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的Map
- 受查视图
- List safeStrings = Collections.checkedList(strings, String.class); ArrayList rawList = safeStrings; rawList.add(new Date()); // checked list throws a ClassCastException
Properties
- 一个非常特殊的映射结构
栈 Stack
位集 BitSet
- 由于位集将位包装在字节里。所以,使用位集要比使用Boolean对象的ArrayList更加高效。
- 案例:查找所有的素数
六、并发
线程
- 多进程和多线程的区别
- 本质区别在于每个进程拥有自己的一整套变量,而线程则共享数据
- 开启一个线程
- 方案一:实现Runnable接口
- lambda方式
- Runnable r = ()->{System.out.println(“I have a dream.”);}; Thread t = new Thread®; t.start();
- 内部类方式
- Thread t = new Thread(new Runnable(){ @Override public void run() { System.out.println(“This is inner class”); } }); t.start();
- lambda方式
- 方案二:构建一个Thread类的子类
- 内部类方式
- new Thread(){ @Override public void run() { System.out.println(“I love you.”); } }.start();
- 继承的方案
- class MyThread extends Thread{ @Override public void run() { System.out.println(“This extend Thread”); } }
- Thread thread = new MyThread(); thread.start();
- 内部类方式
- 方案一:实现Runnable接口
中断线程
- 每个线程都应该不时地检查这个标志,以判断线程是否被中断
- 发送中断请求 interrupt()
- interrupt() 方法用于请求终止线程。当对一个线程调用interrupt方法时,线程的中断状态将被置位
- 异常
- 在一个被阻塞的线程(调用sleep或wait)上调用interrupt方法时,阻塞调用将会被Interrupted Exception异常中断。
- 如果中断状态被置位时调用sleep方法,它不会休眠。相反,它将清除这一状态(!)并抛出InterruptedException
- 检查当前线程,并将中断状态重置为false
- static boolean interrupted()
- boolean isInterrupted()
- 检查当前线程是否被终止
线程的状态
- New 新创建
new Thread®
- Runnable 可运行
一旦调用start方法,线程处于runnable状态
- 被阻塞线程和等待线程
实际上被阻塞状态与等待状态时有很大不同的
- Blocked 被阻塞 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。 当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变为非阻塞状态。
- Waiting 等待 在调用Object.wait 方法或Thread.join 方法,或者等待java.util.concurrent 库中的Lock或Condition时,就会出现这种情况。
- Timed waiting 计时等待 带有超时参数的方法: Thread.sleep、Object.wait、Thread.join、Lock.tryLock以及Condition.await的计时版
- Terminated 被终止 原因一: run方法正常退出而自然死亡; 原因二: 因为一个没有捕获的异常终止了run方法而意外死亡
- 被阻塞线程和等待线程
实际上被阻塞状态与等待状态时有很大不同的
- Runnable 可运行
一旦调用start方法,线程处于runnable状态
线程的属性
- 线程的优先级
- 在Java程序设计语言中,每一个线程有一个优先级
- 默认情况下,一个线程集成它父线程的优先级
- 可用setPriority方法提高或降低任何一个线程的优先级
- 每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。
- 线程优先级是高度依赖于系统的。不要将程序构建为功能的正确性依赖于优先级。
- static void yield() 导致当前执行线程处于让步状态
- 守护线程
- 守护线程的唯一用途是为其他线程提供服务。
- 例如:计时线程,它定时发送“计时嘀嗒”信号给其他线程或清空过时的高速缓存项的线程。
- 当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。
- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
- setDaemon(boolean isDaemon),标记为守护线程,该方法必须在线程启动之前调用
- 守护线程的唯一用途是为其他线程提供服务。
- 未捕获异常处理器
- 当线程发生异常终止的时候,异常会被传递到一个用于未捕获异常的处理器。
- 处理器必须实现一个Thread.UncaughtExceptionHandler接口的类
- 这个接口只有一个方法:void uncaughtException(Thread t, Throwable e)
- 安装线程处理器
- 可以使用setUncaughtExceptionHandler()方法为任何线程安装一个处理器
- Thread t = new Thread(()->{int i=1/0;}); t.setUncaughtExceptionHandler((tt, e)->{System.out.println(“There has exception.”);}); t.start();
- 也可以用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器
- 可以使用setUncaughtExceptionHandler()方法为任何线程安装一个处理器
同步
- 背景:在多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。
- 进行同步的两种机制
- 方案一:synchronized关键字,同步方法
- synchronized关键字自动提供一个锁以及相关的“条件”
- 方案二: 使用锁
- 方案三: 同步代码块
- 方案一:synchronized关键字,同步方法
- 锁对象
- 方案二:ReentrantLock类, jdk1.5提供
- ReentrantLock()
- ReetrantLock(boolean fair)
- 创建一个公平策略的锁。一个公平锁偏爱等待时间最长的线程。但这一公平的保证将大大降低性能。所以,默认情况下,锁没有被强制为公平的。
- myLock.lock();// a ReentrantLock object try{ critical section }finally{ // make sure that lock is unlocked even if an exception is thrown myLock.unlock(); }
- 概述
- 锁用于保护代码片段,任何时刻只能有一个线程执行被保护的代码
- 锁可以用户一个或多个相关的条件对象
- 使用锁就不能使用带资源的try语句
- 方案二:ReentrantLock类, jdk1.5提供
- 条件对象
- Condition
- Condition condition = bankLock.newCondition();
- 每个条件对象管理哪些已经进入被保护的代码段但还不能运行的线程。
- bankLock.lock(); try{ while(condition){ condition.await(); } critical section condition.signalAll(); }finally{ bankLock.unlock(); }
- Condition
- synchronized 方法
- 从jdk1.0 版本,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。 也就是说,要调用方法,线程必须获得内部锁的对象锁。
- 案例:
- public synchronized void transfer(int from, int to, double amount) throws InterruptedException{ // 加条件锁 while (accounts[from] < amount) { wait(); } System.out.println(Thread.currentThread()); accounts[from] -= amount; System.out.printf(” .2f from %d to %d”, amount, from, to); accounts[to] = amount; System.out.printf(” Total Balance: .2f%n”, getTotalBalance()); notifyAll(); }
- synchronized 代码块
- private Object lock = new Object();
public void method(){
synchronized(lock){
critical section
}
}
- 这里lock对象被创建仅仅是用来使用每个Java对象持有的锁。
- private Object lock = new Object();
public void method(){
synchronized(lock){
critical section
}
}
- volatile 域
- 在一个域上声明volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
- final变量
- final Map<String, Double> accounts = new HashMap<>();
- 其他线程会在构造函数完成构造之后才看到这个accounts变量。
- final Map<String, Double> accounts = new HashMap<>();
- 原子性
- java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。
- 死锁
- 线程的局部变量
- 使用ThreadLock辅助类为各个线程提供各自的实例
- 例如: SimpleDateFormat不是线程安全的。
public static final SimpleDateFormat dataFormat = new SimpleDateFormat(“yyyy-MM-dd”);
如果两个线程都执行以下的操作:
String dateStamp = dateFormat.format(new Date());
dateFormat 使用的内部数据结构可能会被并发的访问破坏。
- 可以使用同步,但开销很大;也可以在需要的时候创建一个局部的SimpleDateFormat对象,不过这也太浪费了。
- 可以使用以下代码,为每个线程创建一个实例:
- public static final ThreadLocal dateFormat = ThreadLocl.withInitial(()->new SimpleDateFormat(“yyyy-MM-dd”));
- 要访问具体的格式化方法,可以调用: String dateStamp = dateFormat.get().format(new Date());
- java.util.Random类是线程的。但如果多个线程需要等待一个共享的随机数生成器,这会很低效。可以使用ThreadLocal辅助类为各个线程提供一个单独的生成器。
- 不过jdk1.7 提供了一个便利类。只需要做以下调用:
int random= ThreadLoclRandom.current().nextInt(upperBound);
- ThreadLoclRandom.current() 调用会返回特定于当前线程的Random类实例。
- 不过jdk1.7 提供了一个便利类。只需要做以下调用:
int random= ThreadLoclRandom.current().nextInt(upperBound);
- 锁测试与超时
- myLock.tryLock()
- tryLock方法视图申请一个锁,在成功获得锁后返回true,否则立刻返回false,而且线程可以立刻离开去做其他事情。
- myLock.tryLock(100, TimeUnit.MiLLISECONDS))
- myLock.await(100, TimeUnit.MiLLISECONDS))
- myLock.tryLock()
- 读/写锁
- ReentrantReadWriteLock
- 很多线程从一个数据结构读取数据而很少线程修改器数据的话,ReentrantReadWriteLock是十分有用的。
- 步骤
- // 构建对象 priavte ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); // 获取读锁和写锁 private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock();
- 对读方法加读锁
- 对写方法加写锁
- public void transfer(){ writeLock.lock(); try(){} finally{writeLock.unLock();} }
- 弃用stop和suspend方法
- 这两个方法有一些共同点:都试图控制一个给定线程的行为
- stop方法天生不安全,无法知道什么时候调用stop方法是安全,什么时候导致对象破坏
- suspend方法容易导致死锁
阻塞队列
阻塞队列就是线程安全的集合。
- 背景
- 对于实际编程应该尽量远离底层。
- 对于多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。
- java.util.concurrent
- LinkedBlockingQueue
- 容量是没有上边界的,但是也可以选择指定最大容量
- LinkedBlockingDeque
- 是一个双端版本
- ArrayBlockingQueue
- 在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。通常公平性会降低性能,只有在确实非常需要时才使用它。
- PriorityBlockingQueue
- 一个带有优先级的队列,而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的。取元素的操作会阻塞
- DelayQueue
- 实现了Delayed接口。getDelay方法返回对象的残留延迟。负值表示延迟已经结束。元素只有在延迟用完的情况下才能从DelayQueue移出。
- 同时还必须实现compareTo方法,DelayQueue使用该方法对元素进行排序。
- TransferQueue 接口(jdk1.7 增加)
- LinkedTransferQueue
- 这个接口,允许生产者线程等待,直到消费者准备就绪可以接受一个元素。
- 如果生产者调用q.transfer(item) 这个调用会阻塞,直到另一个线程将元素删除。
- LinkedBlockingQueue
- 使用
- 第一类
- 将队列当做线程管理工具来使用,put和take方法
- 第二类
- 当试图向满的队列中添加或从空的队列中移出元素时,add、remove和element操作会抛出异常
- 第三类
- offer、poll、和peek方法
- 这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。
- offer、poll、和peek方法
- 第一类
线程安全的集合
- 高效的映射、集、队列
- java.util.concurrent
- ConcurrentHashMap
- ConcurrentSkipListMap
- ConcurrentSkipListSet
- ConcurrentLinkedQueue
- 确定这样的集合当前的大小通常需要遍历
- java.util.concurrent
- 同步包装器
- 任何集合类都可以通过同步包装器(synchronizationwrapper)变成线程安全的
- List synchArrayList = Collections.synchronizedList(new ArrayList();
- Map<K, V> synchHashMap = Collections.synchronizedMap(new HashMap<K, V>());
- 如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用“客户端”锁定
- synchronized(synHashMap) { Iterator iter = synHashMap.keySet().iterator(); while(iter.hasNext)… }
- 任何集合类都可以通过同步包装器(synchronizationwrapper)变成线程安全的
Runnable、Callable和Future
- Runnable封装一个一部运行的任务,可以将其想象成为一个没有参数和返回值的异步方法。
- Callable与Runnable类型,但是有返回值。Future 保存异步计算的结果
- Callable接口是一个参数化的类型,只有一个方法call
- public interface Callable{ V call() throws Exception; }
- 可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future对象的所有者在计算好之后就可以获得它。
- Callable接口是一个参数化的类型,只有一个方法call
- 分析Future的方法
- get() throws…
- 这个get方法的调用被阻塞,直到计算完成
- get(long timeout, TimeUnit unit) throws…
- 这个方法调用超时时,会抛出TimeoutException异常
- 如果在运行过程中,线程被中断,两个方法都将抛出InterruptedException。如果计算已经完成,那么get方法立刻返回
- get() throws…
- FutureTask 包装器
- 可以将Callable转换成Future和Runnable
线程池
- 使用线池的好处
- 1、减少并发线程的数目;
- 2、一个线程可以多次提供服务;
- 通过执行器类Executor的静态方法来构建线程池
- newCachedThreadPool
- 必要时创建新线程;空闲线程会被保留60秒;
- newFixedThreadPool
- 创建固定数量的线程,空闲线程会一直被保留
- newSingleThreadExecutor
- 只有一个线程的“池”,该线程顺序执行每一个提交的任务
- 预执行器
- newScheduledThreadPool
- 用于预定执行而构建的固定线程池
- newSingleThreadScheduledExecutor
- 用于预定执行而构建的单线程“池”
- 为预定执行或重复执行任务而设计
- newScheduledThreadPool
- newCachedThreadPool
- 实现线程池的步骤
- 第一步:调用Executor类中静态方法创建线程池
- 第二步:调用线程池submit方法提交Runnable或Callable对象;
- 第三步:保存好返回的Future对象
- 第四步:当不再提交任何任务的时候,调用shutdown方法。关闭线程池
- 调用shutdown方法,执行器关闭,不再接受新的任务。当所有任务都完成以后,线程池中的线程死亡。
- 另一个中方法是调用shutdownNow,该池取消尚未开始的所有任务并试图中断正在运行的线程
- 控制任务组
- 有时,使用执行器更有实际意义的原因,控制一组相关任务
- 可以提交很多任务,每个任务使用一种方法处理一个问题,只要其中一个任务得到答案,计算停止(调用 shutdownNow方法)
- invokeAny
- 提供所有对象到一个Callable对象的集合中
- List<Callable> tasks = …; List<Future> results = executor.invokeAll(tasks); for(Future result: results) processFurthrer(result.get())
- ExecutorCompletionService
- 对结果按可获得的顺序保存起来
- 有时,使用执行器更有实际意义的原因,控制一组相关任务
- Fork-Join框架
- 讲一个大任务拆分成一个个小任务,最后将结果汇总
- 步骤
- 步骤一:扩展一个RecursiveTask类,或扩展RecursiveAction类;
- invokeAll方法接收到很多任务并阻塞
- 对每个子任务应用join,并返回其合计的结果
- 第二步:创建ForkJoinPool 对象,并调用invoke方法提交recursiveTask
- join方法返回结果
- 步骤一:扩展一个RecursiveTask类,或扩展RecursiveAction类;
同步器
七、书籍推荐
看以下三本书籍,更有利于理解并发
- 《Java编程思想》
- D瓜哥:推荐先读并发那章
- 《Effective Java》
- D瓜哥:还可以看看中关于并发的描述
- 左耳朵狮子:如果C和OS基础不错,可以直接看effective java
- 现代操作系统 或 操作系统精髓
XMind: ZEN – Trial Version
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/157046.html原文链接:https://javaforall.cn