美文网首页Java 技术
Java基础知识梳理

Java基础知识梳理

作者: 歪歪歪比巴卜 | 来源:发表于2018-03-31 11:18 被阅读43次

    Java 基础知识梳理

    1、Java的基本程序设计结构

    1.1:数据类型

    java中存在8中基本数据类型,其中有四种整型、2中浮点类型、一种表示unicode编码的子都单元的字符类型char和一种表示真值的boolean类型。

    1.1.1 整型

    整型表示没有小数部分的数值,允许正负数。java提供4种整型,如下表:

    byte类型和short类型主要应用于底层文件处理或者需要控制占用存储空间的最大数组。
    note:长整型数字有一个后缀L,十六进制数值有一前缀0x,8禁止有前缀0,从java7开始,加上前缀0b,就表示二进制数。从Java7开始,可以为数值字面量加下划线,使得数值更容易读,1_000_000表示100万

    double的精度是float的两倍、浮点数采用的是二进制。二进制不能准确表示1/10,所以不能出现在不能有舍入误差的金融计算中。应该使用BigDecima。char类型表示单个字符。

    1.1.2 数学函数Math

    Math.sqrt(X) 求X得平方根
    Math.pow(x,a) 计算X的a次幂
    Math.sin() 计算正弦,需要弧度值
    Math.cos() 计算余弦
    Math.log() 计算自然对数
    Math.log10() 计算以10为底的对数。

    1.1.3 数值转换

    可能丢失精度的转换: int->float,double->float,long->float,long->double,double->int
    存在可能丢失精度的转换不能使用自动转换,必须使用强制转换。double转换为int,将截取掉double的小数位。如果要四舍五入,需要使用Math.round()方法。

    2.字符串

    • Java中,每个双引号括起的字符串都是String类的实例
    • 字符串不可修改
    • 字符串常量存储在公共存储池中,通过new创建的字符串对象存储在堆内存中。
    • StringBuffer线程安全效率低。StringBuilder效率高线程不安全。
    • String类的format方法可以格式化字符串。实例:
    String str = String.formate("hello %s,your salary is %d",name,salary)
    

    3. 大数值

    BigDecima和BigInteger可以分别表示任意精度的浮点数和证书。它们使用valueOf方法将一个普通数组转换成大数值对象。因为没有运算符重载。所以仅提供了add,subtract,mutiply等表示和差等操作。

    4.数组

    • 声明数组的方式: int[] a或 int a[]
    • 数组初始化的方式: int[] a=new int[10], int []a=new int[]{....} , int a[]= {....}
    • 在没有显示初始化的情况下,所有的数组元素将初始值设置为其所对应类型的初始值。对象类型的初始值为null.
    • 数组拷贝: 将一个数组变量直接赋值给另一个数组变量,那么这两个数组变量将使用同一个数组对象。只有使用Arrays.copyOf(type[] a,int length)方法,这两个变量才会指向不同的数组对象。
    • 数组排序 Arrays.sort(type[]a)方法可以将一个数组按照快速排序的方式排序。仅限于byte,short,long,float,double,char等数值类型。
    • 查找 Arrays.binarySearch(type v) 使用二分查找找到一个元素在数组中的位置。查找成功返回对应元素的索引,查找失败返回一个负值。
    • 相等测试,使用Arrays.equals比较两个数组。只有在数组长度相等并且数据中各个下标对应的元素相等,两个数组才算是相等。

    5.面向对象

    • 所有的Java对象都是在堆中构造的,构造器总是通过new操作符调用。
    • 方法参数列表中声明的参数为显示参数。而通过this指定的实例域参数为隐式参数
    • 对象型的变量声明默认值为null,但在局部变量中不会有默认值。必须显示指定初始值。
    • final实例域。final修饰的变量要么已经有初始值,要么在构造器执行之后指定初始值。final修饰的变量一般为基本类型或者不可变类(例如String).如果final修饰一个可变类,其修饰意义不大。因为可变类总是能够合法的改变其对象状态。
    • static修饰的变量为静态域。实例域属于实例,而静态域属于类。实例方法中可以直接访问和修改静态域。
    • 静态常量。静态常量是由static和final修饰的。表示 此值不可更改并且属于类
    • 静态方法,由static修饰的方法则为静态方法。静态方法属于类而不属于实例。静态方法没有隐式参数(this)。静态方法不能访问实例变量,静态方法可以访问本类中的静态变量。
    • 注意: 实例对象是可以调用静态方法的,但是该静态方法仍然只能访问类中的静态域不能访问实例域。并且实例对象也可以调用静态域

    6.方法参数

    • Java的传参总是按值调用的。方法中所得到的形参是一个值得拷贝。
    • 由于Java数据类型由基本数据类型和引用数据类型两大类。在参数传递中。一个方法不能修改基本类型的参数的值。但是可以修改引用类型的参数的值。引用类型的值虽然被复制了,但是任然可以通过它改变被复制对象的值。因为被复制的形参和实参引用的是同一块内存地址。
    • 在Java方法参数中有一些规则: 1、一个方法不能修改一个基本数据类型的参数 2、一个方法能够修改一个可变数据类型的状态 3、 一个方法不能让对象引用一个新的对象。

    7. 对象构造

    • 仅当类没有提供任何构造器时,系统才会提供一个默认的无参构造器。
    • 可以在类定义中。直接将初始值赋值给域对象。构造器在执行构造前,先进行域的赋值操作。
    • 如果构造器的第一个参数形如 this(...),这个构造器将调用这个类的另一个构造器。
    • Java通过3种方式初始化域。可以在声明中初始化一个值,可以在初始化块代码块初始一个值,可以在构造函数中初始化一个值。初始化块会先于构造函数执行。所有的静态初始化语句及静态初始化块都按照类定义的顺序执行。

    8.Object

    • 自定义equals方法需要注意以下:
    • 显示参数名为otherObject,其类型为Object
    • 判断当前对象和other对象是否为同一个引用,如果是的话直接返回true(这是一个优化)
    • 判断other对象是否为null,如果是的话直接返回false
    • 通过getClass判断两者是否属于同一个类型,如果不是的话直接返回false
    • 对所需要比较的域进行比较。通过Objects.equals()比较引用类型实现null安全。基本类型使用==
    • 如果在子类重新定义equals,需要定义super.equals(otherObject)
    • hashCode
    • 默认情况下,调用对象的hashCode方法返回该对象的内存地址。
    • 重新一个类的bhashCode方法,需要确保该类的实例调用其hashCode方法不会出现散列码重复.一般可以通过将该实例内其它域对象的hashCode组合形成新的hashCode.Objects.hash()可以接收多个Object类型的对象。并将其hashCode计算出来并组合。
    • 一般重写了equals方法就应该重写hashCode()方法,以便于该对象能够在散列表中应用。

    9.对象包装器与自动拆装

    • 包装类型是不可变对象。不能改变包装在其中的值。
    • 包装类型是final的,不能自定义包装类型。
    • Integer n =3; n++ 对于这种计算,编译器将n拆箱,再进行计算。计算完成后再进行装箱操作。
    • 自动拆装箱是由编译器处理的而不是虚拟机。
    • 想通过Integer实现修改数值参数?不可能。由于Java的基本类型参数是按值传递的。所以修改基本类型参数的值是无效果的。当然,通过Integer也不行。因为Integer是不可变的。如果需要改变数值参数。需要使用org.omg.CORBA包中的IntHolder

    10.枚举类

    • 所有的枚举类型都是Enum类型的子类。这个类包含了一些方法
    • toString方法返回一个枚举值的字符串形式。例如 Size.SMALL.toString()方法返回字符串"SMALL"
    • toString的逆方法是Enum.valueOf("SMALL") 它将一个字符串转换成枚举类型
    • Size.values返回一个包含全部枚举值得数组。

    11.反射

    能够分析类能力得程序称为反射。被广泛应用在JavaBeans中。

    • Java运行时系统始终为每个对象维护着一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。可以通过Class类访问这些信息。通过object.getClass()方法可以获取对象的Class类型的实例。
    • 通过Class类型对象的getName()方法可以获取当前实例的类型名//1
    • 通过Class的forName方法可以利用类型名构造一个Class实例。//2
    • 如果T是任意的Java对象,可以通过T.class获取其对应的Class实例//3
    • Class实例的newInstance()方法可以用于创建一个类的实例 e.getClass().newInstance()。newInstance()方法调用默认无参构造器。如果这个类没有默认无参构造器就会抛出异常。通常将forName与newInstcnce配合使用。可以利用存储在字符串中的类型名创建一个对象。
    11.1 利用反射检查类的结构

    概览: 在java.lang.reflect包中有三个类Field、Method、Constructor分别用于描述类的域、方法和构造器。通过Class类中的getFields,getMethods,getConstructor方法将分别返回类提供的public域、方法、和构造器数组。包括其超类的公有成员。而Class类的getDeclareFields、getDeclareMethods、getDeclaredConstructor方法将分别返回类中声明的全部域、方法和构造器、其中包括私有的成员,但不包括超类的成员。在获取了这三大类型后,可以使用其相应的方法获取更多的信息。列入获取域的类型,获取方法的修饰符,参数类型,返回值等信息。在方法和构造器中。可以通过getModifiers()方法获取获取构造器或者对象对应的修饰符。

    处理域

    • 通过getFiled方法获取域包括父类的共有域
    • 通过getDeclareField获取域包括本类的私有域,返回Field数组
    • 通过getDeclareField获取指定名称的域,返回Field对象
    • 要通过反射获取一个对象的域的实际值。由于受限于Java权限访问的控制,要访问一个域的私有对象,需要先通过Field的setAccessible(true)方法。
    • 通过Field的get(Object obj)方法获取一个obj对象中用Field对象表示的域值。通过set(Object obj,Object newValue)为一个包含Field域的对象的Field域设定一个新的值。

    处理方法

    • Method方法中有个invoke方法。可以利用此方法调用method中封装的方法。invoke方法的方法签名是: Object invoke(Object invoke,Object ...args)。其中第一个参数是隐式参数。代表调用该实例的方法,如果此方法是静态方法。第一个参数就可以被忽略(设置为null)。
    • 获取Method实例。通过Class实例的getDeclareMethod方法可以获取该Class实例中的所有Method实例。也可以指定方法名获取指定方法。为了避免多个同名的方法(重载)而出现的获取到的Method实例不唯一的情况,可以通过Method.getDeclareMethod(String methodName,Class ...paramsClass)指定参数类型,以确保通过方法名能够获取指定的方法。
    • 如果调用invoke方法执行方法实例,当方法实例返回一个基本类型时。它会被包装成一个对象类型。

    12.接口

    • 如果一个类要实现可比较。就需要实现Compare接口并覆盖其compareTo方法。

    • Arrays中提供了sort方法来实现数组的排序。方法签名为: static void sort(Object[] a)
      。它使用归并排序来实现排序。要求数组中的元素都是可比较的,对象类型应当实现Compareable接口。

    • 接口中的方法自动被设置为public,接口中的域自动被设置为public static final

    • 为何要设计接口而不直接采用抽象类。因为Java不支持多继承。

    13.对象克隆

    • 当使用 = 操作符拷贝一个对象时,原始的拷贝只是将两个引用变量引用同一个对象。
    • 如果想要拷贝出一个状态相同的对象,就应该使用对象的clone()方法。
    • Object中的clone()方法是protected的,如果想让对象实现clone()方法的调用。就必须实现clone()方法并将其访问修饰符改变成public。
    • 默认的clone()操作是浅拷贝,它并没有克隆包含在对象的内部对象。如果内部的对象是可变的,那么就必须实现clone()方法。参考C++ operator=操作符重写。
    • 如果要自定义对象的克隆,就必须实现: 1、实现Cloneable接口 2、使用public修饰符修饰重新定义的clone()方法。
    • 一个clone()的实现示例;
    class Employee implements Cloneable{
        public Employee clone() throws CloneNotSupportException{
            //call object.clone()
            Employee coned = (Employee) super().clone();
            //克隆可变对象
            cloned.hireDay = (Date) hireDay.clone();
            ...
        }
    }
    

    需要注意的是。被克隆的子对象必须都实现了Cloneable接口,否则将会抛出CloneNotSupportException异常。

    14.回调

    • Java中没有函数指针,需要传入一个完整的对象供特定事件发生时调用
    • Timer类提供了一个定时执行的功能。Timer(int interval,ActionListener listener)构造一个定时器,每隔interval毫秒钟通告listener一次。

    15.内部类

    • 内部类可以访问其自身的数据域,也可以访问创建它的外部类的数据域。
    • 在外部类的作用于之外,应当使用OutClass.InnerClass方式引用内部类
    • 利用已有的外部类对象创建内部类 outerObject.new InnerClass()
    • 可以将内部类定义在一个方法或代码块中称为局部内部类。局部内部类不能用private或public进行访问和修饰。因为它的作用于被限定在声明这个局部类的块中。
    • 如果使用内部类只是为了将类隐藏在外部内的内部,并不需引用外部类的数据域,为此。可以将内部类设置为匿名内部类

    16.代理

    • 代理可以在运行时创建一组给定接口的新类。这种功能只有在编译时无法确定哪个接口才非常有用。

    17. 异常

    • 所有异常派生自Throwable接口。其下有Error和Exception.而在Exception下,又派生了IOException,RuntimeException等。
    • 如果子类覆盖了超类方法,子类方法中抛出的方法不能比超类抛出的方法更通用,或者不抛出异常。如果超类方法中没有抛出任何异常,覆盖的子类方法也不能抛出任何异常。
    • 如果自定义异常,应该提供至少两个构造方法,一个是无参的构造方法。另一个是包含错误信息的构造方法。
    • 使用异常的性能小号非常大,应当在异常的情况下使用异常。
    • 传递异常,应该让高层方法知道发生了什么并决定如何通知用户。

    18.泛型

    • 泛型: 希望编写的代码能够被其它不同类型所重用。
    • 类型变量的限定: 如果希望类型满足一些条件应该使用类型变量限定,例如希望将T限制为实现了Comparable接口: <T extends Compareable> 限定的绑定类型可以是类也可以是接口。使用关键字extends。如果有多个限定,使用'&'符号分割。
    • 泛型方法的泛型类型声明应当放在修饰符之后(public static)返回值(void)之前。
    • 虚拟机会擦除泛型,所有泛型类最终都会被擦除泛型。使用限定类型,如果没有限定类型就使用Object类型。
    • 不能用基本类型实例化类型参数,因为泛型擦除后会使用Object代替限定类型。Object不能存储基本类型的值。
    • 运行时类型查询只适用于原始类型。因为泛型擦除,无论什么限定类型。在运行时类型查询都为原始类型。例如:
        Pair<Employee> employeePair;
        Pair<String> stringPair;
        employeePair.getClass()==stringPair.getClass();//true
    
    • 不能创建参数化类型的数组。如:
        Pair<T> [] pairs = new Pair<T> [10];
    

    泛型擦除后,将会存储Object类型的对象。数组将会记住它的类型。导致真正的元素类型无法插入

    • 泛型类的静态上下文中类型变量无效。不能在静态域和静态方法中引用类型变量。

    • 不能抛出也不能捕获泛型类的实例。实际上,泛型类扩展Throwable都是不合法的。

    • 通配符类型可以继承限定,也可以超类型限定。这是同泛型限定所不同的地方。例如:<? extends Employee><? super Employee>

    19.集合

    • 集合的iterator()方法将返回一个Iterator接口.调用这个接口的的hasNext()方法确认是否还有元素。调用这个接口的next()获取集合中的下一个元素。调用remove()方法删除元素。需要注意的是,要删除一个元素。必须先用next()方法越过这个元素,如果在调用remove()元素前并未调用任何next()方法,就会抛出IllegalStateException。
    • Java中的链表都是双向链表
    • LinkedList的add()方法将元素添加到链表末尾。如果需要将元素添加到链表中间,就需要利用迭代器将指针遍历到指定位置后再执行添加。
    • ListInterator是Iterator的子接口。在LinkedList中,通过listIterator()方法获取。与Iterator相比,ListIterator方法支持add()操作。此外,它有hasPrevious()与previous()方法用于反向遍历列表。set()方法是用一个新的元素取代调用next()或previous()方法的上一个元素。
    • 链表的随机访问能力差,不应该使用LinkedList作索引访问,也不应该频繁的使用get()。更不应该使用索引循环遍历链表。如果要遍历链表,应该使用迭代器或者foreach而不是索引循环。因为每次调用get(i)方法都会有一次O(n)的线性搜索。尽管LinedList的get()方法作了优化(索引大于size/2时从后往前搜索).
    • LinkedList实现了Queue,可以作队列使用.LinkedList有addFirst(),addLast(),removeFirst(),removeLast(),getFirst,getLast()等方法。实际上可以完成队列或者栈的操作。
    • ArrayList不是同步的,Vector是线程同步的。如果在不考虑多线程的情况下,应考虑使用ArrayList,避免使用Vector带来的同步开销。
    • 散列表为每个对象计算一个整数,称为散列码。在Java中,散列码是根据Object.hashCode()方法计算的。散列码必须与equals兼容。如果a.equals(b)为true,那么a.hashCode()==b.hashCode()就应该为true。
    • HashSet是散列表的一种实现。只有在不关心元素顺序并且元素不重复时应该使用HashSet。散列表中索引的位置即为(元素的散列码%桶的总数)
    • TreeSet是有序的。遍历TreeSet时。元素能够以它插入的顺序被遍历出来。 TreeSet是红黑树实现的。
    • Queue可在尾部添加元素,头部删除元素。Dequeue是双端队列。支持同时在头部和尾部添加或删除元素。ArrayDequue是循环数组实现的无限双端队列。
    • 优先队列(PriorityQueue)使用‘堆’实现,堆是一种颗完全二叉树。使用优先队列的典型场景是任务调度。
    • Java使用Map实现映射表的数据结构。Java Map提供了两个通用实现: HashMap和TreeMap HashMap对键进行散列,TreeMap对使用搜索树对键进行排序。HashMap的性能比TreeMap好,再不要求对键进行排序时,应当使用HashMap。
    • 如果对于同一个键调用两次put方法,第二个值就会替代第一个值。
    • Set<K> keySet()方法可以获取一个Map中的所有键的集合。Collection<V> values可以获取一个Map中的所有值。Set<Map.Entry<K,V>>可以获取Map中K,V组成的所有EntrySet。EntrySet中有getKey()和getValue()方法分别获取键值。
    • Map包含containsKey()与containsValue()方法用于检查一个key或者value是否包含在Map中。
    • Collections类中的sort方法可以对实现了List接口的集合进行排序。使用归并排序对列表中的元素进行排序。被排序的列表的元素应该实现了Comparable接口。如果需要手动指定排序方式,可以创建一个Comparator对象作为参数传递给sort方法。
    static <T> void sort(List<T> elements,Comparator<? super T> c)
    
    该排序算法的复杂度是O(n log n)
    
    • Collections类提供了shuffle(List<T> elements) 静态方法用于打乱一个列表的内部元素顺序。
    • Collections类提供二分查找算法,诸如: ststic <T> int binarySearch(List<T> elements,T key)。 将返回查找到元素的索引。如果返回负值,表示没有匹配到任何值。二分查找会检查List接口是否实现了RadomAccess接口。如果实现了这个接口,将使用二分查找。否则将使用线性查找。
    • Collections提供了以下常用的简单算法:
      • T min(Collection<T> elements): 获取集合中最小的元素
      • T max(Collection<T> elements): 获取集合中最大的元素
      • void copy(List<? super T> to,List<T> from): 将原列表的所有元素复制到目标元素的相应位置上,目标列表的长度至少与原列表一样。
      • boolean addAll(Collection<T> collections,T...values):将所有给定元素添加到目标集合中。添加成功返回true。
      • boolean replaceAll(List<T> l,T oldVlue,T newValues): 将所有oldVlue的元素替换成newValue
      • revcerse(List<T> l):倒置列表中的索引。

    20. 多线程

    • 进程与线程的区别: 进程拥有自己的内存和变量,进程间的变量与内存相互隔离。而线程则共享同一套变量

    • Runnable接口。Runnable接口用于封装一个任务,Runnable接口中有一个run方法。可以把run任务封装进run方法。可以将runnable作为Thread构造方法的参数。Thread运行时将调用Runnable的run方法。不要直接调用Thread类或者Runnable类的run方法。直接调用run方法并不会开启一个新的线程去执行任务。应该使用start方法

    • 线程终端。如果相手动中断一个线程,应该调用线程的interrupt方法,这是每一个线程都具有的boolan标识。线程运行时会随时检查它的状态。当在一个被阻塞的线程(例如wait或者sleep)上调用interrupt方法时。阻塞调用将会被InterruptedException。有两个类似的方法: isIntercepted和intercepted方法,前者是实例方法,用于检查当前线程程是否被终端。后者是静态方法,检查当前线程是否中断,并且在中断过后会清空当前线程的中断状态。

    • 获取当前的线程: Thread.currentThread()

    • 线程的状态

      • New(新创建) 当用new操作符创建一个新线程时,如new Thread(r)。该线程还没开始运行,这意味着它的状态是new
      • Runnable(可运行): 一旦调用线程的start方法,线程将处于runnable状态。需要注意的是,一个可运行的状态可能正在运行也可能没有运行。尤其是,正在运行的线程的状态也是Runnable
      • Blocked(被阻塞): 当一个线程视尝试获取一个内部的对象锁,而该锁被其它线程持有时。该线程进入阻塞状态
      • Wating(等待状态)
      • Timed Waiting (计时等待): 例如调用Thread.sleep方法。Thread.join,Object.wait方法时线程进入计时等待状态
      • 通过Thread.getState()方法获取一个线程的状态
    • Java线程优先级依赖于操作系统(某些操作系统不支持线程优先)。通过setPriroty()方法设置优先级。优先级的值为1~10

    • Thread.yield()静态方法。该方法导致该线程处于让步状态,如果有其它与它同样高的优先级的的线程,那么这些线程接下来会被调度。

    • 守护线程。通过setDaemon(boolean isDaemon)方法来标识一个线程为守护线程。这一方法在线程启动之前调用。守护线程的唯一用途就是为其它线程提供服务,例如一个计时线程定时通知其它线程去清理缓存。

    • +=操作会被拆分成两步来执行。因此它不是原子操作,在多线程共享访问中。会出现线程安全问题

    • Java有两种机制同步代码块。一是synchronized,二是ReentrantLock对象

    • ReentrantLock利用lock()方法来锁住临界区,一旦临界区被该线程锁住,其它线程就无法进入临界区。只有当持锁线程释放锁时,其它线程才有机会进入。

    private ReentantLock myLock = new ReentrantLock();
    myLock.lock()
    try{
        // do something
    }finally{
        myLock.unLock();
    }
    

    注意: 以银行转账的Bank对象为例,为保证原子操作。需要对Bank对象中的交易逻辑使用同步机制。每一个Bank对象都有一个对应的ReentantLock对象,只有多个线程访问同一个Bank对象时才会串行化访问。而访问不同的Bank对象时则互不影响。

    • 锁的可重入性:所谓重入,指lock()方法可以嵌套调用。被一个锁保护的代码可以直接调用另一个使用相同锁的方法。此时,锁的持有计数增加。直到持有计数变为0的时候,释放锁。
    • 条件对象: 线程可能需要一些条件才能执行某些操作,当线程获取了锁过后。发现没有执行操作而必须的条件。此时,线程就会放弃锁并进入等待状态。直到另一个线程调用同一条件对象的signAll()方法去唤醒为止。需要注意的是,如果没有另一个线程去调用同一个条件对象的signAll()方法,线程将死锁。Java提供的条件类:Condition 条件对象不能自己创建,它属于一个指定的ReentantLock对象,通过锁对象的newCondition()方法来获取这个锁对象的条件对象。
    class Bank{
        private Condition condition;
        ...
        public Bank{
            ...
            condition = bankLock.newCondition();
            if(...){
                condition.await();
            }else{
                ...
                condition.signAll();
            }
        }
    }
    

    调用signalAll()方法不会立即激活await的线程,只是解除了该线程的阻塞。

    • 可以使用一种嵌入到Java语言内部的机制来实现同步,Java中的每一个对象都有一个内部锁和内部条件。如果一个方法用synchronized关键字声明,那么该对象的内部锁将保护整个方法。
    • 内部锁条件用wait方法将一个线程置入等待集中,使用notify,notifyAll方法唤醒线程。wait,notify,notifyAll方法都是Object的final方法。一般来说,如果线程不满足一个条件,将被放入等待集中。
    • 将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁,如果A类有一个静态同步方法。那么调用调用该方法时,A.class对象的锁被锁住。
    • 线程可以通过进入一个同步方法获得锁,还可以通过进入同步阻塞获得锁。例如,线程进入如下形式的阻塞:
    synchronized(obj){
        //TODO ...
    }
    

    进入此同步阻塞将获得obj的锁。

    • volatile为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域可能被另一个线程并发跟新。不过volatile变量不提供原子性,例如+=操作和!取反操作。
    • 线程局部变量: 在线程间共享变量有风险,可以使用ThreadLocal类为各个线程提供自己的实例。SimpleDateFormate不是线程安全的,可以为各个线程都提供一个SimpleDateFormate实例。ThreadLocal<T>有如下的方法:
    T get(): 得到这个线程当前的值,如果是首次调用get,会调用initialize来得到这个值。
    protected initialize():应用覆盖这个方法来提供一个初始值。默认情况下,这个方法返回null。
    void set(T t): 为这个线程设置一个新值。
    void remove(): 删除对应这个线程的值
    

    以下演示利用ThreadLocal提供SimpleDateFormate线程变量:

    public static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>{
        protected SimpleDateFormat initalValue(){
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    }
    
    //使用
    String formateDate=dateFormat.get().format(new Date())
    

    此外,Random虽是线程安全的,但是多个线程公用一个Random类会很低效。Java7提供了ThreadLocalRandom.current()调用会返回特定于当前线程的Random类实例。

    • **读写锁: ** Java提供了ReentrantReadWriteLock读写锁类,它通关过readLock()获取一个读锁,通过writeLock()获取一个写锁。读锁可以被多个读操作公用,但会排斥所有的写操作。写锁会排斥其它所有的读操作和写操作。如下示例:
    public double getTotalBalance(){
        readLock.lock();
        try{
            ...
        }finally{
            readLock.unLock();
        }
    }
    
    public void tansfer(){
        writeLock.lock();
        try{
            ...
        }finally{
            writeLock.unlock();
        }
    }
    
    • 阻塞队列
      使用队列,可以安全的从一个线程向另一个线程传递数据。生产者线程向队列插入元素,消费者队列从队列中取出它们。使用队列,可以安全的从一个线程向另一个线程传递数据。以银行转账程序为例,转账线程将转账指令对象插入一个队列中。而不是直接访问银行对象,另一个线程从队列中取出指令执行转账,只有该线程可以访问该银行对象的内部。因此不需要同步。
      当阻塞队列作为线程管理工具来使用时,就需要响应队列为空或者队列已满的状态。当队列为空时,使用队列的add,remove方法会抛出异常,而阻塞队列提供了offer(添加一个元素并返回true),poll(移除并返回队列的头元素,如果队列为空则返回null),peek(返回队列的头元素,如果队列为空则返回null)。它使用错误提示而不是抛出异常。因此,向队列中插入null是非法的。此外,还有带有超时参数的offer方法和poll方法。

    • Runnable与Callable
      Runnable接口封装的是一个异步运行任务,可以把它想象成一个没有参数和返回值的异步方法。Callable与Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法call

        public interface Callable<V>{
            V call() throws Exception;
    

    例如Callable<String>表示一个最终返回String类型结果的异步计算。

    • Future

    Future保存异步计算的结果。可以启动一个计算,将Feture对象交给某个线程然后忘掉它,Feture线程的所有者再计算好结果之后就可以获得结果。

    public interface Future<V>{
        V get() throws Exception;
        V get(long timeout,TimeUnit,unit);
        void cancel(boolean mayInterrupt);
        boolean isCancelled();//
        boolean isDone();//如果完成了返回true
    }
    

    有两个get方法的实现,第一个get方法调用将被阻塞,直到计算完成。第二个get方法的调用如果超时,将会抛出TimeoutException。如果计算完成,get()方法将立即返回。如果运行该计算的线程被中断,那么两个方法都将抛出InterruptException。

    • FutureTask
      FutureTask将Callable包装并转换为Future或者Runnable
        Callable<Integer> cb;
        FutureTask<Integer> ft = new FutureTask<Integer>(cb);
        Thread t = new Thread(task) //包装成Runnable
        t.start
        ...
        IntegerResult result = task.get(); //包装成Future
    
    • 线程池
      构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。将Runnable对象交给线程池,就会有一个线程调用run()方法,当run方法退出时。线程不会死亡,而是在线程池中为下一个请求提供服务。线程池的另一个作用是限制并发线程的数目。

    执行器(Excutor)类有许多静态工厂方法来构建线程池,首先有如下三个:

        newCachedThreadPool();
        newFixedThreadPool();
        newSingleThreadExcutor();
    

    newCachedThreadPool()方法创建一个可缓存的线程池,对于每个任务。如果有空闲的线程可用。立即用它执行线程,如果没有空闲的任务可用,则创建一个新的线程。newFixedThreadPool方法构建一个具有固定大小的线程池。如果提交的任务数多余空闲的线程数,则把没有得到执行的任务放置到队列当中。当其它任务完成后再运行它们。newSingleThreadExcutor是一个大小为1的线程池。由一个线程执行提交的任务。这三个方法返回实现了ExcutorService接口的ThreadPoolExcutor对象。可以用下面的三个方法之一将一个Runnable对象或者一个Callable对象提交给ExcutorService:

        Future<?> submit(Runnable task);
        Future<T> submit(Runnable task,T result);
        Future<T> submit(Callable<T> task);
    

    其中第一个方法返回一个Future对象,可以使用这个对象来调用isDone、isCancel、isCancelled方法,但是使用get方法只是简单的返回null,第二个方法返回的Future对象的get方法返回指定类型的result对象;第三个方法传入一个Callable,并且在返回的Future对象在计算结果准备好的时候得到。

    • 关闭线程池
      当用完一个线程池时,调用ExcutorService的shutDown方法将启动该池的关闭序列,此后将不再接收新的任务,并保证未完成的任务完成后关闭。如果需要忽略未完成的任务,可以直接调用shutdownNow方法关闭线程池。

    21. IO

    • 抽象类InputStream流和OutputStream构成了IO流的基础
    • InputStream类定义了抽象方法read(),这个方法读入一个字节,并返回读入的字节。或者在遇到输入源结尾时返回-1。InputStream还有许多非抽象的方法,例如读取一个字节数组、从指定偏移量读取。这些方法都将调用read()方法,因此。实现具体的输入流只需要覆盖InputStream的read()方法。
    • OutputStream定义了一个类似的抽象方法write()
    • read和write方法在执行时都将阻塞。直至字节流确实被读入或者写出。这就意味着如果流不能被立即访问(通常因为网络繁忙),当前线程就会被阻塞。
    • 当完成对一个流的输入或读写时,应该通过调用close()方法来关闭它。关闭一个输出流的同时还会冲刷用于该输出流的缓冲区。如果不关闭文件,那么写出字节的最后一个包可能永远也得不到传递(除关闭外,可以手动的调用flush()关闭)。
    • 流可以分为处理字节的流和处理字符的流。InputStream和OutputStream构成了处理字节的流的基本抽象。Reader和Writer构成了处理字符的流的抽象。
    • FileInputStream是用于读取文件的输入流。它具有两种构造。一是FileInputStream(String name),二是FileInputStream(File file) 前者使用文件名来构造一个文件输入流(非绝对路径将按照VM启动时所设置的工作目录来解析)
    • FileOutpuyStream的构造如下:
        FileOutputStream(String name);
        FileOutputStream(String name,boolean append);
        FileoutputStream(File file);
        FileoutputStream(File file,boolean append);
    

    需要注意的是,在FileoutPutStream中,如果append为true,那么数据将会被添加在文件尾,而具有相同名字的文件不会被覆盖。否则,会覆盖具有相同名字的文件。

    • BufferedInputStream(InputStream in) 创建一个带缓冲区的流,带缓冲区的输入流从流中读取字符时,不会每次都对设备进行访问。当缓冲区为空时,会向缓冲区读入一个新的数据块。BufferedOutputStream采用类似的机制。
    • 为字符输入流指定编码:
      使用如下的方式对一个字符输入流指定编码:
        InputStreamReader in = new InputStreamReader(new FileInputStream("readme.txt"),'utf-8')
    
    • 读入一个文本输入
      利用一个BufferedReader读取文本输入的方式如下:
        BufferedReader bReader = new BufferedReader(new InputStreamReader(new FileInputStream('readme.txt'),'utf-8'));
        String line;
        while(line=in.readLine()!=null){
            //do something
        }
    

    相关文章

      网友评论

        本文标题:Java基础知识梳理

        本文链接:https://www.haomeiwen.com/subject/rprgcftx.html