Java编程的逻辑

Java SenLin 4年前 (2020-03-25) 532次浏览 已收录 0个评论

零 概述

本篇文章是读《Java编程逻辑》的笔记,原文也可以看作者博客。

第1章 编程基础

  1. 为了操作数据方便,定义了数据类型和变量;数据计算的过程中还需要进行流程控制,所以有了控制语句;为了减少重复代码和分解复杂操作,引入了函数和子程序的概念。
  2. 数据类型和变量
    数据类型为了对数据归类,以便于理解和操作。Java的基础数据类型有4中大类型:
  • 整数类型:有四种整型,byte/short/int/long,分别有不同的取值范围;
  • 小数类型:两种float/double,有不同的取值范围和精度;
  • 真假类型:boolean,表示真假。
    基本类型也有对应的数组类型,数组表示固定长度同种数据类型的多条记录,在内存中连续存放
  1. 内存在程序看来就是一块有地址编号的连续空间,数据放到空间之后为了方便查找和操作,需要给位置起个名字——用变量来表示这个概念。变量的变是指内存空间里面存的值是可变的。但是变量的名称不变,这个名称是程序员心中的内存空间的意义——应该是不变的,如果你一定要个age变量里面放身高的值也是可以的。

小结:变量就是给数据起名字,方便找不同的数据,值可以变但是含义不应该变。

  1. 赋值。赋值就是给变量对应的空间设一个值,注意给float赋值的时候加F或f,因为小数默认是double类型。
  2. 数组类型。数组的长度确定之后就不能变化了。数组有一个length属性,但是是只读属性。如果数组的初始值确定就不能再改长度,因为这样会让计算机无所适从。例如一下代码:
int [] a= new int[3]{1,2,3}

数组和基本类型不一样,基本类型占一块内存空间,而数组需要两块:一块用于存储数组内容本身——第一个值的内存空间,另一块存储内容位置。

  1. 数组的比较是判断两个变量指向的同一个数组,而不是判断元素内容是否一样。
  2. 逻辑运算符。
  • 异或(^):两个相同为false,两个不同为真。
  • 或(|)与短路或(||)、与和短路与(&&)的区别是短路的只要第一个根据第一个可以得出结果,就不会执行第二个。例如下面代码
int b=0;
boolean flag= true|b++>0;
int b=0;
boolean flag= true||b++>0;

第一段b最后结果为1,第二个最后结果为0。既第二个只要得出结果就不会求后面的。

  1. switch的类型为byte,short,int,char,枚举和String,为什么没有float、double和long呢?这和switch的实现原理有关。switch的转换和具体的系统实现有关。如果分支比较小,可能会转换为跳转语句,如果分支比较多,会转换为跳转表——跳转表高效的原因是其中的值必须为整数——String通过hashCode方法转换为整数,且按大小排序可以使用二分查找。如果值是连续的或者比较密集,跳转表可能会优化为一个数组。跳转表的存储空间为32位,所以不能使用long类型,存储不下;没有float和double是因为存储的是整数。
  2. 函数的主要目的是减少重复代码和分解复杂操作。定义函数只是定义了一段有着明确功能的子程序,但是本身不会执行,需要调用
    定义函数时声明参数,实际上就是定义变量,只是这些变量的值是未知的,调用函数时传递参数,实际上就是给函数中的变量赋值。
  3. 函数参数里面比较特殊的是数组类型和可变长度。数组类型赋值的时候赋值的是数组存储自身内容本身的空间,所以修改会影响外面。可变参数实际上是变为了一个数组。例如下面两端代码等同。
public static int max(int min,int ... a){
//anything
}
public static int max(int min,int [] a){
//anything
}
mac(0,new int[]{2,4,5})

可变参数简化了代码书写。

  1. 函数调用
    函数调用是通过栈来存储相关的数据——变量和下一条执行语句等,系统就调用者和函数如何使用栈做了约定,返回值可以简单的认为是通过一个专门的返回值存储器存储的。从这个过程中可以看出,函数的调用是有开销成本的——分配额外的栈空间存储参数、局部变量以及返回地址,还需要进行出栈和入栈操作。所以函数大小的划分需要衡量。
  2. 数组和对象的内存分配
    数组和对象的内存由两块组成,一块存放实际内容,一块存放实际内容的地址。存放实际内容的空间一般不分配在栈上,而是分配在堆上。存放地址的空间分配在栈上。

第2章 理解数据背后的二进制

  1. 二进制的最高位表示符号,1为负数、0为正数。但是二进制的实际表示方法是补码表示法,例如byte类型的-1:1的原码是00000001,取反是11111110,然后加1就是11111111。
    知道了最高位为符号位,那么就可以推算出为什么byte的最大值为127,最小值为-128了。8位2进制的模为256,正数和负数各占一半,00000000到01111111表示正数,10000000到11111111表示负数。这个计算过程复杂,但是为了简单的记范围区间的算法-2n-1~2n-1 -1。

关于这一点,网上大多数关于补码、反码的讨论都无法计算出-128,或者计算逻辑有问题,常见csdn的博客。本书作者也没有去说这一块,可以参考知乎文章,或者2.1.2 数的机器码表示,这里从起源到计算都论述到了。

  1. 为什么计算机无法精确表示0.1?因为计算机只能表示2n的和,例如0.5=2-1、0.75=2-1+2-2。如果不是特别追求精度,可以使用四舍五入等方法。如果追求精度可以转换为正数,求完值之后再转回小数。
  2. 为什么叫浮点数?这是由于在二进制表示中,表示那个小数点的时候,点不是固定的而是浮动的。在十进制中表示123.45,直接这么写就是固定的。如果使用1.2345E2那就是小数点向左浮动了两位。二进制中表示小数也采用类似科学计数法,形如m*(2e),m称为尾数,e称为指数。在二进制中,单独表示尾数部分和指数部分,另外还有一个符号位表示正负。
    几乎所有的硬件和编程语言表示小数的二级制格式是一样的——IEEE754标准。一种是32位(Java的float),一种是64位(Java的double)。32位格式中,1位表示符号,23位表示尾数,8位表示指数。64位格式中,1位符号、53位尾数、11位指数。Java中想查看浮点数的具体二进制形式,可以使用如下代码:
Integer.toBinaryString(Float.floatToIntBits(value));
Long.toBinaryString(Double.floatToIntBits(value));
  1. char本质上是一个占用固定两个字节的无符号整数——Java采用UTF-16BE编码,这个正整数表示Unicode编号,用于表示那个Unicode编号对应的字符。关于更多编码文章,可以看java中一个汉字占几个字符。

第3章 类的基础

  1. 类的属性和方法分为:类所有也叫静态,静态成员变量和静态方法;实例所有的变量和方法。
  2. 静态初始化代码块在类加载的时候执行,这是在任何对象创建之前,且只执行一次。
  3. 私有构造方法
    构造方法可以是私有方法,使用场景可能有3种:
    1)不能创建类的实例,类只能被静态访问,如Math和Arrays类,他们的构造方法是私有的;
    2)能创建类的实例,但只能被类的静态方法调用。一种常见的场景:类的对象有但是只能有一个,即单例。在这种场景中,对象是通过静态方法获取的,而静态方法调用构造方法创建一个对象,如果对象已经创建过了,就重用这个对象——我在创建线程池的时候使用过。
    3)只是用来被其他多个构造方法调用,用于较少重复代码。

2018-06-04更新

第四章 类的继承

  1. 子类对象赋值给父类引用变量,这叫做向上转型。
    一种类型的变量,可以引用多种实际类型对象叫做多态。声明时的类型成为静态类型,实际的类型为动态类型,在运行过程中调用对应动态类型的方法,称为动态绑定。
Shap[] shaps = new Shap[3];
shaps[0]=new Circle();
shaps[0]=new Line();
//anything
shaps[0].draw();

上面代码中Shop类型是shops静态类型,Circle、Line是动态类型,shaps[0].draw()是动态绑定。
那为什么需要有多态和动态绑定呢?因为创建对象的代码和操作对象的代码经常不在一起,操作对象的代码往往只知道对象是某种父类型,也往往需要知道它是某种父类型就可以了。
多台和动态绑定是计算机的一种重要思维方式,使得操作对象的程序不需要关注对象的实际类型,从而可以统一处理不同对象,但又能实现每个对象的特有行为。

  1. 每个类肯定有一个父类,且只能有一个父类。没有声明的默认为Object。
  2. 在new过程中,父类先进行初始化。如果没有通过super调用父类的构造方法,会自动调用父类默认方法,如果父类没有默认方法则会报错——编译错误
  3. 父类构造方法中如果调用了可被重写的方法,则可能会出现意想不到的的结果。例如
public class Base {
    public Base() {
        test();
    }

    public void test() {
    }
}
public class Child extends Base {
    private int a = 123;
    private Integer b = 456;

    public Child() {
    }

    public void test() {
        System.out.println(a);
        System.out.println(b);
    }

    public static void main(String args[]) {
        Child c = new Child();
        c.test();
    }
}

Base调用了被子类重写的方法test(),输出结果为

0
null
123
456

因为Base调用了被子类重写的方法test(),在Child构造方法调用之前先调用了父类的方法,父类方法调用了被子类重写的test方法,但是这时候子类构造方法和变量赋值语句还没有被执行,所以输出默认值0和null;后面子类实例化和赋值之后输出了真实的值。所以在父类的构造方法中,应该只调用private的方法。

  1. 继承中重命名和静态绑定
    实例变量、静态变量、静态方法、private方法,都是静态绑定的——即访问绑定到变量的静态类型
    private的变量:子类访问子类的,父类访问父类的;public变量和方法:在类内,访问的是当前类的,但是子类可以通过super.明确指定访问父类的。在类外,则要看访问类的静态类型——声明类型,静态类型是父类访问父类的,静态类型是子类访问子类的。
  2. 重载与重写
    重载是改变方法签名里面的参数签名;重写是子类对父类方法的重写。
    当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先需要按照参数类型进行匹配,换句话说,寻找在所有重载版本中最匹配的,然后才看变量的动态类型进行动态绑定——当子类重载了父类的方法,先判断参数类型然后再判断对象是子类还是父类引用。

例如父类有sum(int a,int b)方法,子类有sum(int a,long b)方法,创建子类的对象c调用sum方法,执行c.sum(1,2),那么调用的是父类的sum(int a,int b)方法。

  1. 父子类型转换
    子类型可以向上转型为父类型,但是父类型不一定可以转换为子类型。能不能转换主要取决于父类型的动态类型是不是这个子类或子类的子类型——常用的就是把Object转子类型。
  2. protected是继承权限,当然还包括了包级别权限。
  3. 可见性重写
    继承时可见性是不变或增大的,因为继承表达的是“is-a”的关系,即子类肯定是父类,所以不能降低或减少父类对外的行为。
  4. 防止被继承final
  5. 对象创建的过程
    创建对象包括:
    1)分配内存
    2)对所有实例变量赋默认值
    3)执行实例初始化代码
  6. 方法调用的过程
    寻找要执行的实例方法的时候,是从对象的实际类型信息开始查找的,找不到的时候再查找父类类型的信息——从重写的作用上也可以看出来这一点。动态绑定也是一样。
    这个过程,如果继承层次比较深,当要调用的方法位于比较上层的父类,则调用的效率是比较低的,大多数系统采用一种虚方法表的方法来优化——就是在类加载的时候为每个类创建一个表,记录该类的对象所有动态绑定的方法(包括父类的方法)及其地址,但一个方法只要一条记录,子类重写了父类方法就只保留子类的。
  7. 对变量的访问是静态绑定的——不论是类变量还是实例变量。
  8. 为什么继承是把双刃剑
    1)继承破坏封装。封装是为了隐藏细节,但是继承很多时候却需要了解父类的实现,父类改变的时候要去关注子类的实现。子类关注父类的改变主要是父类可重写方法之间的互相调用,这样子类重写的时候可能就破坏父类的方法。
    2)继承没有反映is-a关系。继承本来设计用来反映is-a关系的,但是现实中设计完全的is-a关系很难。例如鸟类作为父类,那么有个fly()方法,但是偏偏企鹅不会飞,你可能想企鹅不会飞但是会游泳,那么fly()方法就实现为了游泳。本来是为了is-a关系设计的继承,但是Java没法确保这种关系。父类的方法和属性子类可能不一定都适用,子类实现可能和父类预期不一致。
  9. 如何应对继承的双面性
    1)避免使用继承
  • 使用final关键字
  • 使用组合而非继承——在类中定义要继承类的变量
  • 使用接口
    2)正确适用继承
  • 基类别人写的——重写方法不要改变预期行为;阅读文档说明,理解可重写方法实现机制,尤其是方法之间的依赖关系;在基类改变的情况下,阅读改变修改子类
  • 我们写基类,别人实现——使用反应真正的is-a关系,只将真正公共的部分放到基类中;对不希望继承的使用final修饰符;写文档,为别人提供说明,指导子类如何编写
  • 我们写基类,写子类——可以兼顾第一和第二点,但是自己写适当可以放开一点。

2018-06-05更新

第五章 类的扩展

1.接口的本质
如果只是将对象看做属于某种数据类型,并按该类型进行操作,在一些情况并不能反应对象以及对象操作的本质。很多时候我们更关心对象的能力
接口就是用来表示能力的一种方式。

  1. 接口最重要的是降低了耦合,提高了灵活性。
  2. 接口中的变量
    接口中的变量是public static final的,即使不写也是这样的。类似于的接口中的方法,写不写都是public的。
  3. 接口的继承
    接口可以继承,且可以有多个父接口。例如 public Interface extends IBase1,IBase2
  4. Java 8 和Java 9对接口的增强
    在Java 8 和Java 9 中都允许接口有静态方法和默认方法。但是Java 8 里面只允许public,Java 9取消了这种限制,提高了方法的复用能力。
  5. 抽象类和抽象方法
    只有子类知道如何实现的方法一般定义为抽象方法,例如接口里面的非默认和静态方法。
    抽象类就是抽象的类,抽象相对于具体而言的,一般而言具体类具有直接对应的对象,而抽象类没有,它表达的是抽象的概念。
    有抽象方法的类一定是抽象类,但是抽象类可以没有抽象方法。抽象类可以和具体类一样定义实例变量、具体方法等,但是他不能创建对象。
    如果一个类继承了抽象类,必须实现所有的抽象方法,除非他自己是抽象类。
  6. 为什么需要抽象类
    对于抽象方法,如果不知道如何实现为什么不定义一个空方法体?不让抽象类创建对象,看上去也只是增加了一个不必要的限制。
    实际上抽象类和抽象方法都是Java的一个语法工具,通过强制措施来引导使用者正确使用它们,在编译阶段就发现问题。
    无论编程和日常生活,每个人都可能会犯错,减少犯错不能只靠个人素质,还需要一些机制使得每个普通人都容易把事情做对。这就是抽象类设计的初衷。
  7. 抽象类和接口
    抽象类和接口有相同的地方,也有不同的地方。接口不能定义实例变量,而抽象类可以
    抽象类和接口是配合不是替代关系,他们经常一起配合使用。接口提供能力,抽象类提供默认实现——实现全部或部分方法,一个几口经常有一个默认实现类。例如:
  • Collection接口和对应的AbstractCollection类
  • List接口和对应的AbstractList抽象类
  • Map接口和对应的AbstractMap抽象类
    对于需要实现接口的具体类而言,有两个选择:实现接口需要重写所有方法;继承抽象类根据需要重写非抽象方法。如果具体类已经有父类,那么就只能实现接口。

2018-06-06

  1. 内部类的本质
    一个类放在另外一个类的内部,这个类就叫做内部类。
    内部类和包含它的外部类有比较密切的关系,和其他类关系不大,定义在内部,可以实现对外部完全隐藏,有更好的封装性,代码实现也往往更为简洁。
    内部类只是编译时的概念, 在编译的时候会被编译两个文件。
public class OuterClass {
    private String outerName;
    private int outerAge;
    public class InnerClass{
        private String innerName;
        private int innerAge;
    }
}

编译后外部类及其内部类会生成两个独立的class文件: OuterClass.class和OuterClass$InnerClass.class
内部类可以方便的使用外部类的私有变量,把内部类声明为private从而实现对外完全隐藏,相关代码写在一起,写法更简洁。
内部类分为:

  • 静态内部类
    静态内部类的使用场景很多,如果它与外部类联系密切且不依赖于外部类实例,则可以考虑定义为静态内部类。
    静态内部类除了在类内部,其它和定义普通类没有区别,它可以访问外部类的静态变量、静态方法,但是不能访问实例变量和方法——和普通类的静态方法类似,静态的只能访问静态的。静态类可以被外部访问,使用“外部类.静态内部类”的方式。
  • 成员内部类——与静态内部类相比,成员内部类没有static修饰符。除了静态变量和方法,成员内部类还可以直接访问外部类的实例和方法。如果内部类和外部类关系密切,需要访问外部类的实例变量或方法,则可以考虑定义成员内部类。
  • 方法内部类——在一个方法里定义和使用。如果方法是实例方法,则除了静态变量和方法,内部类还可以直接访问外部类的实例变量和方法。如果是静态方法,则只能访问外部类的静态变量和方法。方法内部类可以直接访问方法的参数和方法中的变量,但是这个变量是final或者隐形final——不能被赋值,不然编译错误。
  • 匿名内部类——范围最小,不能在外部使用。匿名内部类没有单独的类定义,在创建对象的同时定义类。语法如下:
new 父类(参数列表){
  //匿名内部类实现部分
}

new 父接口(){
  //匿名内部类实现部分
}

匿名内部类只能被使用一次,用来创建一个对象,没有名字没有构造方法,但是可以根据参数列表调用对应的父类构造方法。因为没有构造方法,所以无法接受参数。与方法内部类一样,匿名内部类也可以访问外部类的所有变量和方法。
匿名内部类也会被生成一个独立的类,但是命名是通过父类加数字编号,没有有意义的名字。匿名内部类可以做的,方法内部类都可以。如果对象只会创建一次且不需要构造方法来接受参数,这样可以使用匿名内部类,代码更为简洁。
将程序分为保持不变的主题框架,和针对具体情况的可变逻辑,通过回调的方式进行写作,是计算机程序的一种常用实践。匿名内部类是实现接口回调的一种简便方式。

总结:内部类本质上都会被转换为独立的类,是一种编译上的概念,在代码编译的时候会变为两个文件。但一般而言,它们可以实现更好的封装,代码实现上也更为简洁。


2018-06-10

10 枚举的本质
在switch的语句内部(case语句)枚举值不能带枚举类型前缀。枚举类型也都有一个静态的values方法,返回一个包括所有枚举值的数组,顺序与声明时的顺序一致。
枚举的好处:

  • 定义枚举的语法简洁
  • 枚举更为安全。取值范围要么为一个枚举类型,要么为null,不会超出可控。
  • 枚举类型有很多便利方法。
    枚举类型会被编译器编译为一个类,继承了java.lang.Enum。有name和ordinal两个实例变量,枚举类型的方法都是通过name和ordinal两个变量实现的。下面是Size枚举编译后的代码:
public final class Size extends Enum<Size>{
  public static final Size SMALL = new Size("SMALL",0);
  public static final Size MEDIUM = new Size("MEDIUM",1);
  public static final Size LARGE = new Size("LARGE",2);
  private static final Size VALUES = new Size[]{SMALL,MEDIUM,LARGE};

  private Size(String name,String ordinal){
    super(name,ordinal);
  }

  public static Size[] values(){
    Size[] values = new Size[VALUES.length];
    System.arraycopy(VALUES,0,values,0,VALUES.length);
    return values;
  }
  
  public static Size valueOf(String name){
    return Enum.valueOf(Size.class,name);  
  }
}

通过编译后的类,可以发现:

  • Size类是final的,不可以被继承。
  • Size有一个私有构造方法,所以不能够被外部调用创建新对象。
  • 三个枚举值实际上是三个静态变量
  • values方法是编译器添加的,内部有一个valuse数组保持所有枚举值
  • valuseOf方法是调用的父类方法。

在switch语句中,枚举值会被转换为其对应的ordinal值——switch的跳转表最多有32位空间,那么创建一个枚举类,其中类型有long种。这样是不是就会报错?不会,因为在构造枚举的时候就报错了,等不到switch语句。枚举类型编译后的私有构造方法接受的ordinal参数为int。


2018-06-11

第六章 异常

  1. 异常栈信息就包括了从异常发生点到最上层调用者的信息,还包括行号。Java默认的异常处理机制是退出程序,异常发生点后的代码都不会执行。
  2. throw关键字可以与return关键字进行对比。return代表了正常退出,throw代表了异常退出;return的返回位置是确定的,throw后执行哪行代码则经常是不确定的,有异常处理机制动态确定——异常处理机制从当前函数开始查找看谁“捕获”了这个异常,当前海曙没有就查看上一层,直到主函数,如果主函数没有就使用默认异常机制,即输出异常栈信息并退出。
  3. Throwable
    throwable是所有异常类的父类,有四个构造方法:
  • public Throwable()
  • public Throwable(String message)
  • public Throwable(String message,Throwable cause)
  • public Throwable(Throwable cause)
    Throwable类的两个主要参数是message和case。message表示异常信息,case表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层异常触发,case表示底层异常。Throwable还有一个public 方法用于设置cause:
Throwable initCause(Throwable cause)

Throwable的某些子类没有带cause参数的构造方法,可以通过这个方法来设置,该方法最多只能调用一次。

  1. Throwable有两个子类:Error和Exception。
    Error表示系统错误或资源耗尽,有Java系统自己使用,应用程序不应抛出和处理。
    Exception表示应用程序错误,它有很多子类,可以通过集成Exception或其子类创建自己的异常类。
  2. 受检异常(checked)和未受检异常(unchecked)
    受检异常和未受检异常的区别在于Java如何处理这两种异常。受检异常Java会强制要求程序员进行处理,否则会发生编译错误,而对未收件异常则没有这个要求。例如RuntimeException是未受检异常,Exception的其他子类则是受检异常。

Exception的子类中RuntimeException及其子类都是未受检异常,其他的子类或其子类都是受检异常。所以在创建自己的子类异常时,注意继承的父类。

  1. finally
    finally的代码不管有无异常发生,都会执行,具体来说:
  • 如果没有异常发生,在try代码执行结束后执行
  • 如果有异常发生且被catch,在catch内代码执行结束后执行
  • 如果有异常且没有被捕获,则在异常抛给上层之前执行
    由于finally的这个特点,所以往往当做资源释放来使用。
  1. finally的执行细节
  • 如果在try或者catch语句内有return语句,则return语句在finally执行结束后才执行,但finally不会改变返回值,例如:
public static int test(){
  int ret=0;
  try{
    return ret;
  }finally{
    ret=2;
  }
}

因为在执行到try内的返回语句return前,会将返回值ret保存在一个临时变量中,然后才执行finally语句,当finally语句执行结束后,再返回临时语句的值,所以finally修改的ret不会被返回。

  • 如果finally中也有return语句呢?try和catch语句里面的return会丢失,实际会返回finally中的返回值。finally中有return不仅会覆盖try和catch里面的返回值,还会掩盖try和catch内的异常,就像没有异常发生一样。例如:
public static int test(){
  int ret=0;
  try{
    //ArithmeticException 分母为0
    int a=5/0;
    return a;
  }finally{
    return 2;
  }
}

因为finally语句中有return语句,所以这个方法就会返回2,异常不会再往上层传递。如果finally中抛出了异常,则原异常也会被掩盖。

为避免混淆,应该避免在finally中使用return语句或者抛出异常,如果调用其他代码可能抛出异常,则应该捕获异常并进行处理。

  1. try-with-resources
    java 7开始支持一种新的语法,称之为try-with-resources,这种语法针对实现了java.lang.AutoCloseable的接口对象。在try中声明资源,Java 9取消了这种限制,但是必须是final或者事实上的final。示例代码如下:
try(AutoCloseable r = new FileInputStream("hello")){
  //anything
}
  1. throws
    异常机制中,还有一种和throw很像的关键字throws,用于一个方法可能抛出异常,语法如下所示:
public void test() throws AppException,SQLException{
}

对于未受检异常可以不用声明,但是对于受检异常必须通过声明——换句话说,如果没有声明不能抛出。因为声明是告诉调用者,这些异常没有处理,需要处理。当然声明了也可以不抛出异常。

  1. 受检异常和未受检异常

受检异常必须出现在throws语句中,调用者必须处理,Java编译器会强制这一点,而未受检异常则没有这个要求。
关于受检异常和未受检异常,一种普遍的说法是:未受检异常表示编程的逻辑错误,编程是应该避免这种错误,例如npe,当发生这种问题的时候,程序员应该考虑检测代码bug而不是处理这种异常。受检异常表示代码没有问题,由于一些其他问题导致的,调用者应该进行适当处理。
但实际中未受检异常也需要处理,因为Java程序往往运行在服务器中,不能因为一个逻辑错误就退出。
更加实用的一种观点是:无论受检异常还是未受检异常,无论是否出现在throws声明中,应该在合适的地方以适当的方式处理,并且保持同一个项目的一致性。

  1. 如何使用异常
    异常应该且仅用于异常情况。异常不能替代正常的条件判断,例如知道某个值可能未null,可以提前判断。真正出现异常的时候应该抛出异常,而不是返回特殊值——但是如果异常的处理结果就是特殊值,那就返回特殊值。
    处理异常的目标可以分为恢复和报告。恢复是指通过程序自动解决问题,报告的最终对象可能是用户、运维人员或者程序员。报告的目的是为了恢复。
  2. 异常处理的一般逻辑
    如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要向上报告。

2018-06-12

第七章 常用基础类

1.包装类
包装类就是只对基本类型的包装,例如Integer,Boolean等。
包装类与基本类型的转换代码结构是类似的,每种包装类都有一个静态方法valuseOf(),接受基本类型,返回引用类型,也都有一个xxxValue()返回基本类型。

  1. 自动装箱和拆箱
    自动装箱和拆箱是编译器提供的能力,背后他会替换为对应的valuseOf()/xxxValue(),例如
Integer a =100;
int b=a;

会被编译器替换为

Integer a = Integer.valueOf(100);
int b=a.intValue();
  1. 静态方法valueOf和new
    在使用中是使用静态方法valueOf还是new一个新对象?一般建议使用valueOf方法,new 每次都会创建一个新对象,而除了Float和Double外的其他包装类,都会缓存包装类对象,减少需要创建对象的次数,节省空间提升性能。Java9开始,这些构造方法已经被标注为过时的,推荐使用静态的valuseOf方法。
  2. 包装类重写Object类的方法
    1)重写equals方法
    Object的equal方法是比较的存储地址,重写之后的比较的是基本类型的大小。Float和Double使用了floatToIntBits(把float的二级制看成int二进制)和doubleToLongBits的方法比较大小。
    2)重写hashCode
    hashCode返回一个对象的哈希值。哈希值是一个int类型的数,由对象中一般不变的属性映射得来,用于快速对对象区分、分组等。一个对象的哈希值不能改变,相同对象的哈希值必须一样。不同对象的哈希值一般应不同,但这不是必须的。例如学生对象可以用生日作为哈希值,不同学生生日一般不同,哈希值比较均匀,个别生日相同也没有关系。
    hashCode和equals方法联系密切,对两个对象如果equals方法返回true,那么hashCode必须一样。反之不要求,equals返回false时,hashCode的值必须为true。
    hashCode的默认实现是把对象存储空间地址转换为整数,子类如果重写了equals方法,也必须重写hashCode方法。之所以有这个规定,是因为Java api中很中类依赖这个行为,例如容器中的一些类。
    包装类都对hashCode进行了重写,其中Byte、Short、Integer、Character的hashCode为其的内部值;Boolean的为:
public int hashCode(){
  return value?1231:1237;
}

选用两个质数,质数用于哈希比价好,不容易冲突。
Long是最高32位和最低32位的异或操作,Double为把二进制看做Long类型之后最高32位和最低32位的抑或操作。Float是把二进制看做Integer的值。

  1. 包装类实现Comparable
    Comparable类只有一个方法compareTo,当但前对象与参数进行比较的时候,当小于、等于、大于参数时分别返回-1、0、1。
  2. Number类
    6种基本数据类型都有一个父类Number,Number是一个抽象类,他定义了如下方法:
byte byteValue();
short shortValue();
int intValue();
long longValue();
float floatValue();
double doubleValue();
  1. 不可变性
    包装类是不可变类,即对象创建之后就无法修改了,通过以下方式强制实现:
  • 所有包装类都是final的,不能被继承;
  • 所有基本类型值都是私有的,且声明为final;
  • 没有定义setter方法。
  1. 包装类的valueOf实现
    以Integer为例,他的valuseOf方法如下:
public static Integer valueOf(int i){
  assert IntegerCache.high>=127;
  if(i>= IntegerCache.low && i<=IntegerCache.high)
    return IntegerCache.cache[i+(-IntegerCache.low)];
  return Integer(i);
}

IntegerCache是一个私有静态内部类。其中IntegerCache有两个属性low和high,high可以通过配置指定,默认是127,low无法改变,默认为-128。
如果没有默认指定,那么IntegerCache会缓存-128~127之间的整数,因为包装类是不可变的,所以直接返回缓存值即可。


2018-06-13

  1. String
    String内部用一个字符数组表示字符串,实例变量定义为:
private final char value[];

String有两个构造方法:

public String (char value[]);
public String (char value[],int offset,int count);

String会根据参数新创建一个数组,并复制内容,而不会直接用参数中的字符数组。

  1. String不可变性
    String类是不可变类,即对象一旦创建,就没有办法修改了。String也声明为了final,不能被继承,内部char数组也是final的,初始化就不能再变了。
  2. 字符串常量
    在内存中,字符串常量放在一个共享的地方,叫做字符串常量池。它保存所有的常量字符串,每个常量只会保存一份,被所有共享者共享。当通过常量使用一个字符串的时候,使用的就是常量池中的哪个对应的String类型的对象。例如
String name = "测试";
String name2="测试";
System.out.println(name==name2);

输出结果为true,因为name和name2都使用的是常量池里面的对象。但是通过new创建的就不是的了。例如:

String name = new String("测试");
String name2 = new String("测试");
System.out.println(name==name2);

输出结果就是false了。name和name2指向两个不同的String对象,其中两个对象里面的value值指向同一个char数组。

  1. StringBuilder
    StringBuffer和StringBuilder都一样,但是StringBuffer是线程安全的。
    StringBuilder内部的char数组不是final的,是可变的,默认长度为16。当长度不够的时候会按指数扩展,会在当前长度乘以2再加上2。所以如果可以预期字符串长度的情况下,应该在创建StringBuilder的时候指定长度。

在不知道最终需要多长的情况下,指数扩展是一种常见的额策略,广泛应用于各种内存分配相关的计算机程序中。

  1. String的+和+=运算符
    String的+和+=运算符实际上编译器转换为StringBuilder,然后使用append。例如:
String hello = "hello";
hello+=",world";
System.out.println(hello);

一般会被Java编译器转换为:

StringBuilder hello = new StringBuilder("hello");
hello.append(",world");
System.out.println(hello.toString());

既然Java会直接编译,为什么还要自己使用StringBuilder呢?因为编译器没那么智能,可能会创建多个StringBuilder。


2018-06-17

第8章 泛型

  1. “泛型”的字面意思就是广泛的类型。类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码可以用于多种数据类型。
  2. 泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。
  3. Java的伪泛型
    Java伪泛型是指Java把泛型分为两部分:编译器和虚拟机。Java的泛型在编译的时候会把泛型替换为Object,然后使用类型强制转换。在虚拟机执行的时候,不会感知到泛型,只知道普通的类及代码。
  4. 泛型的好处
  • 更好的安全性
  • 更好的可读性
    语言和程序设计的一个重要目标是将bug尽量消灭在摇篮里,能消灭在写代码的时候就不要留到运行的时候。通过泛型,编辑器、编译器会提前发现问题,不会在运行的时候发现。

强类型语言的好处。弱类型语言很难在编写的时候发现这种问题。

  1. 方法是否泛型和类是否泛型无关。方法的泛型声明如下:
    Java编程的逻辑
    image.png

图片来自杨元博客

在返回值前面放置一个类型参数<T>。
和泛型类不同,泛型方法调用时一般不需要特意指定类型参数的实际类型,Java编译器可以自动推断出来。

  1. 类型的参数限定
    Java可以支持限定类型参数的上界,通过extends关键字来表示,这个类可以是具体的类或者具体的接口,也可以是其他类型参数。
    指定边界后,类型擦除时就不会转换为Object了,而是会转换它的边界类型。
    1)上限为某个接口
    例如限定类型为Comparable接口:
public static <T extends Comparable> T max(T[] arr){
  T max = arr[0];
  for(int i=0;i<arr.length;i++){
    if(arr[i].compareTo(max)>0){
      max = arr[i];
    }
  }
}

但是这么写编译器会报错,因为Comparable是一个泛型接口,它也需要一个泛型参数。所以应该为:

public static<T extends Comparable<T>> T  max(T[] arr)

<T extends Comparable<T>>是一种令人费解的语法形式,这种形式称为递归类型限制,可以这么解读:T是一种数据类型,必须实现Comparable接口,且必须可以与相同类型的元素进行比较。
2)上界为其他类型参数

public class DynamicArray<E> {
    public void addAll(DynamicArray<E> c) {
        for (int i = 0; i < c.size; i++) {
            add(c.get(i));
        }
    }
}

但是这种写法有一些局限性,例如:

DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
numbers.add(34);
numbers.addAll(ints);

Integer是Number的子类,这种使用方法感觉合情合理,但是为什么不行呢?看下面代码:

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<Number> numbers = ints;//假设这行是合法的
numbers.add(new Double(12.34));

ints中本来准备存储的是Integer,但是里面却存进去了Double,显然破坏了泛型的类型安全。
因为Integer是Number的子类,但是DynamicArray<Integer>不是DynamicArray<Number>的子类。
如果想把Integer添加到Number所在容器中,这个问题可以通过类型限定来解决——把方法也变为泛型的。例如:

public <T extends E> void addAll(DynamicArray<T> c){
//实现代码
}

E是DynamicArray的类型,T是addAll的类型,T的上界限定是E,这样就可以了。

  1. 泛型总结
    泛型是计算机程序中一种重要的思维方式,他将数据接口和算法与数据类型相分离,使得同一套数据结构和算法能够应用于各种数据类型,而且可以保证类型安全,提高可读性。
  2. 更简洁的参数类型限定
    使用通配符,可以更加简洁。例如前面提到的将Integer对象添加到Number容器中的代码,可以写成:
public void addAll(DynamicArray<? extends E> c){
  //方法体
}

这个方法没有定义类型参数(方法有泛型方法变为了普通方法),c的类型为DynamicArray<? extends E> c,“?”表示通配符,”<? extends E>”表示有限定通配符,匹配E或者E的子类,具体什么类型是未知的。
统一使用了extends关键词,<T extends E>和<? extends E>有什么区别:
1)<T extends E>用于定义类型参数,声明了一个类型参数T,可放在泛型类定义的类名后面,泛型方法的返回值前面。
2)<? extends E>用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道他是E或者E的某个子类型。

<T extends E>指明了T是上界E或者E的子类;<? extends E> 中?是一种具体的类型参数。

  1. 除了前面提到的有限定通配符,还有一种无限定通配符<?>,无限定通配符和使用类型参数<T>是一样的。
  2. 虽然通配符形式更为简洁,但是两种通配符都有一个限制——只能读,不能写——addAll方法里面也是读。
    原因是这样的,通配符不像类型参数那样指定的是一种,而是一到多种,编译器完全无法确定到底传入的是哪一种。这种情况不符合Java类型安全规范,例如下面代码:
DynamicArray<? extends E> numbers = new DynamicArray<>();
numbers.add(new Double(23.0));
numbers.add(new Interge(21));
  1. 类型参数和通配符的总结
    1)通配符都可以用类型参数来替代,通配符能做的,类型参数都能做。
    2)通配符形式上可以减少类型参数,形式上简单可读性好,所以能用通配符就用通配符
    3)如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数
    4)通配符和类型参数往往配合使用,定义必要的类型参数,通配符表示依赖,并接受更广泛的数据类型。
  2. 超类型匹配符
    有一种和<? extends E>正好相反,它的形式为<? super E>,称之为超类型通配符,E表示某个父类型。有了这个,我们可以更灵活的写入了。
    超类型匹配符无法使用类型参数替代。
  3. 通配符比较
    前面介绍了三种通配符,<?>、<? extends E>、<? super E>,它们之间的关系是:
    1)它们的目的都是是方法接口更为灵活,可以接受更为广泛的类型。
    2)<? super E>用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数替代。
    3)<?>和<? extends E>用于灵活读取,使得方法可以读取E或者E的子类容器对象,它可以被类型参数替代,但是通配符更为简洁。
  4. 使用泛型类、方法和接口时的细节和局限性
    1)基本类型不能用于实例化类型参数——因为编译时,会被替换为Object,所以不能使用基本类型。
    2)运行时类型信息不适用于泛型——因为编译之后,泛型就不存在了
    3)类型擦除可能会引发一些冲突——例如使用泛型重载方法,编译后都为Object。
  5. 定义泛型类、方法和接口时的细节和局限性
    1)不能通过类型参数创建对象
    例如
T elm = new T();

如果允许,那么用户以为创建的是具体类型的对象,但是类型擦除后都为Object,所以为了避免误解Java直接禁止。
2)泛型类型参数不能用于静态变量和方法
因为静态变量和方法是属于的类的,且与类型参数无关。如果可以创建,那么每个实例都会有一份静态变量和方法。但是由于类型擦除,所有类都只有一份。
这里说的是类型参数不能用于静态变量和方法,不是说静态方法就不能有自己的类型参数,这个参数与泛型类的类型参数是没有关系的。
3)了解多个类型限定的语法——支持多个上界,上界之间用&符号连接。例如

T extends Base & Comparable & Seriablizable

Base为上界类,如果有上界类,类应该放在第一个,类型擦除是,会用第一个上界替换。

  1. 泛型与数组
    不能创建泛型数组
    前面提到过,类型参数之间有继承关系的容器之间没有关系,例如DynamicArray<Integer>对象不能赋值给DynamicArray<Number>变量。但是数组是可以的,例如
Integer[] ints = new Integer[10];
Number[] numbers = ints;
Object[] objs = ints;

这种赋值关系Java是支持的,数组是Java直接支持的概念,所以他知道数组元素的实际类型,知道Object和Number是Integer的父类,所以可以的。虽然Java支持这种操作,但是使用不当会发生运行时异常,例如

objs[0]="hello";

编译没有问题,但是运行时会抛出ArrayStoreException。因为Java知道实际类型是Integer,但是写入String就不行。
如果允许创建泛型数组,那么数据擦除之后,编译不会报错运行也不会报错。但是如果使用不当,代码的其他地方就会爆雷——把实际A类型当做B类型使用。而且这种非常隐蔽。所以Java为了防止发生这种误解,就拒绝创建泛型数组。
对泛型和数组的关系总结之后为:

  • Java不支持创建泛型数组
  • 如果要存放泛型对象,可以使用原始类型的数组,或者使用泛型容器
    例如泛型类Pair<T,U>,原始类型数组应该为:
Pair[] = new Pair[]{new Pair<String,Integer>("1元",7)};
  • 泛型容器内部使用Object数组,如果需要转换泛型容器为对应类型的数组,需要使用反射。

top8488大数据 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:Java编程的逻辑
喜欢 (0)
[]
分享 (0)

您必须 登录 才能发表评论!