J2SE知识总结

API文档

JDK API

  • 什么是 JDK API

    API文档是我们用来了解JDK中提供的类库,我们可以先通过索引输入并找到我们需要了解的类,而后我们就可以方便的了解该类的作用,常量的作用,以及该类提供的所有方法的作用,以及方法的参数及返回值的含义。

  • JDK包结构

    1. JDK包是由sun开发的一组已经实现的类库,里面有非常丰富的功能类。可以为我们的开发提供各种支持。
    2. JDK根据提供的功能不同,将类库划分为若干个包,比如用于操作输入输出的java.io包,java程序语言设计基础类的java.lang包,提供各种数学运算的java.math包,基于网络应用的java.net包,以及一些共用程序类所在的java.util包等等。

    jdk包结构

文档注释规范

  • 文档注释

    通过注释提高Java源程序代码的可读性;使得Java程序条理清晰,易于区分代码行与注释行。另外通常在程序开头加入作者,时间,版本,要实现的功能等内容注释,方便后来的维护以及程序员的交流。

  • 文档注释规范

    规范1

    规范续1

    Javadoc命令生成文档

    命令生成文档

字符串基本操作

String及其常见API

  • String是不可变对象

    由于字符串在实际开发中被广泛使用,那么在频繁使用某个字符串时,会出现频繁创建一个字符串对象的现象,java为此对字符串的使用采用了一个优化措施,使得String对象为不可变对象,一旦在内存中创建,内容不能发生变化,若要对字符串内容改变,那么就会创建新对象。这样做的目的是可以最大程度的重用相同内容的字符串以减小系统资源的开销。

  • String常量池

    JVM对字符串有一个限制,让字符串作为不变对象,这样就可以做到重用。事实上,当我们通过字面量,常量来初始化一个字符串时,JVM首先会从字符串的常量池(一个JVM内部维护的内存区域,用来保存已经创建过的字符串对象)中查询用来保存该字符串的对象是否存在,若存在则直接引用,若不存在则创建该字符串对象并存入常量池,然后引用它。因为字符串内容不能改变,所以我们可以放心的重用他们。

  • 内存编码及长度

    1. java存储每一个字符均使用2个字节保存,使用的是Unicode编码。并且任何一个字符(无论是英文还是汉字)每个字符的长度都是1。所以字符串的长度就是该字符串所有的字符个数。
    2. int length():返回当前字符串的长度。
  • 使用indexOf实现检索

    int indexOf(int ch):用来检查给定的一个字符在当前字符串中第一次出现的下标位置。这里的下标和数组的下标意思相近,0表示该字符串的第1个字符,以此类推。当该字符串中并不包含给定的字符时,那么该方法返回-1。 例如:

        String str = "HelloWorld";
        System.out.println(str.indexOf('W'));//5
        System.out.println(str.indexOf('h'))//-1
    
  • 使用substring获取子串

    String substring(int begin,int end):用来截取当前字符串的部分内容以获取这个子字符串。我们只需要传入两个整数,一个用来表示从哪里开始,另一个用来表示截取到哪里。这里的位置要使用字符串的下标来表示,并且要注意,这两个数字表示的范围是“含头不含尾的”,换句话说就是包含开始下标的字符,但是不包含结束下标的字符。

  • trim

    String trim():将字符串两边的空白(空白有很多种,空格是其中之一)去除掉,并将去除后的新字符串返回给我们。

  • charAt

    char charAt(int index):用于给定一个下标位置,来获取该字符串中这个位置的字符。

  • startsWith和endsWith

    1. boolean startsWith(String suffix):用来判断当前字符串是否是以给定的字符串开始的。这里要注意大小写是敏感的。

    2. boolean endsWith(String suffix):用来判断当前字符串是否是以给定的字符串结尾的。

    3. 例如我们可以使用endsWith()就可以根据一个文件的名字来判断它是否是以".jpg",".gif"等字符串结尾来得知该文件是否为图片。

       String str = "java.jpg";
       if(str.endsWith(".jpg")){
           System.out.println("是一张图片");
       }else{
           System.out.println("不是一张图片");
       }
      
  • 大小写变换

    1. String toUpperCase():用来将当前字符串中的英文部分的字符全部变为大写后再将新的字符串返回
    2. String toLowerCase():用来将当前字符串中的英文部分的字符全部变为小写后再将新的字符串返回
  • valueOf

    字符串提供了很多重载的valueOf()方法,可以将其他基本类型的值以字符串的形式描述。

      static String valueOf(int i): 返回 int 参数的字符串表示形式
      static String valueOf(boolean b): 返回 boolean 参数的字符串表示形式
      static String valueOf(char c): 返回 char 参数的字符串表示形式
      static String valueOf(double d): 返回 double 参数的字符串表示形式
      static String valueOf(char[] c): 返回 char 数组参数的字符串表示形式
      static String valueOf(char[] c,int offset,int count): 返回 char 数组参数的特定子数组的字符串表示形式。
      static String valueOf(float): 返回 float 参数的字符串表示形式
      static String valueOf(long l): 返回 long 参数的字符串表示形式
      static String valueOf(Object o): 返回 Object 参数的字符串表示形式
    

StringBuilder及其常用API

  • StringBuilder封装可变字符串

    1. String类我们已经得知,它是不变对象,那么每当对内容修改时都会引发新对象的创建。那么当我们有一个需求是需要频繁修改字符串时,这样不仅不能减少内存的开销,反而会增加内存的开销。为此java为我们提供了一个专门用于修改字符串内容的类:StringBuilder.
    2. 该类封装了可变的字符串,换句话说,当我们需要改变字符串内容时,并不会创建新对象,而是在原对象基础上进行修改。从而减小了内存的开销。
  • StringBuilder常用方法

      append(String str):追加字符串;
      insert (int dstOffset,String s):插入字符串;
      delete(int start,int end):删除字符串;
      replace(int start,int end,String str): 替换字符串;
      reverse():字符串反转。
    
  • StringBuilder

    1. StringBuilder的很多方法的返回值均为StringBuilder类型。这些方法的返回语句均为:return this。也就是可以做链式调用

    2. 由于改变封装的字符序列后又返回了该对象的引用。

       buf.append("ibm").append("java").insert(3, "oracle").replace(9, 13, "JAVA");
       System.out.println(buf.toString());
      
  • StringBuilder 总结

    1. StringBuilder是可变字符串。字符串的内容计算,建议采用StringBuilder实现,这样性能会好一些。

    2. java的字符串连接的过程是利用StringBuilder实现的,代码如下所示:

       String s = "AB";  String s1 = s + "DE"+1;
       String s1 = 
       new StringBuilder(s).append("DE").append(1).toString();
      

    StringBuffer 和StringBuilder的区别:

      StringBuffer是线程安全的,同步处理的,性能稍慢;
      StringBuilder是非线程安全的,并发处理的,性能稍快。
    

正则表达式

基本正则表达式

  • 正则表达式简介

    所谓正则表达式就是使用一系列预定义的特殊字符来描述一个字符串的格式规则,然后使用该格式规则匹配某个字符串是否符合格式要求。

      1、“.”和"\"
    
      "."点儿,在正则表达式中表示任意一个字符。
      "\"在正则表达式中是转意字符,当我们需要描述一个已经被正则表达式使用的特殊字符时,我们就可以通过使用"\"将其转变为原本的意思。
      "\"在正则表达式中也有一些预定义的特殊内容:
      \d:表示任意一个数字
      \w:表示任意一个单词字符(只能是 数字,字母,下划线)
      \s:表示任意一个空白字符(\t \r \n \f \x0B)
      \D:表示任意一个非数字字符
      \W:表示任意一个非单词字符
      \S:表示任意一个非空白字符
    
      2、"字符集合 []"
    
      "[]"用来描述单一字符,方括号内部可以定义这个字符的内容,也可以描述一个范围。例如:
      [abc]:表示该字符只能是a或者b或者c
      [123]:表示该字符只能是1或者2或者3
      当我们需要描述所有小写字母时,我们可以使用范围 [a-z],表示该字符可以是任意一个小写字母。
      同样还可以使用 [0-9] 来表示该字符可以是任意一个数字。
      也可以在多个范围内选择。比如,[a-zA-Z0-9_] 表示该字符可以是任意字母,数字以及"下划线"。
    
      3、"*"、"+"、"?"
    
      通常我们需要描述的字符串会有很多重复出现的元素,但又不需要严格限制出现的次数时,我们就可以使用"*","+"这些量词。
      例如:邮箱地址,那么在"@"字符前允许出现若干字符作为用户名。这时候我们就可以使用"\w+"来描述这里至少出现一个单词字符了。
      "+":表示内容可以连续出现至少1次以上
      "*":表示内容出现0-若干次
      "?":表示内容出现0-1次
    
      4、{n}、{n,}{n,m}
    
      除了前面讲到的量词外,有时我们也需要要求内容出现的次数有具体要求。比如手机号码。
      这时我们要求出现的数字就不能是一个模糊的概念了,而必须要求11位。
      又比如我们要求用户输入密码时,要求密码是6-15位。遇到这类问题是,我们可以使用:
      {n}:表示内容必须出现n次
      {n,m}:表示内容出现n-m次
      {n,}:表示内容出现至少n次
      例如,\d{11} 就表示数字只能出现11位,这样就解决了上述的问题。
    
  • 分组

      通过上面的内容,我们还无法解决类似下面的问题:
      在描述电话号码时,前面有区号,区号的形式可以是0086或者+86
      那么我们如何在这两个字符串之间选择?
      这时我们可以使用分组"()"。() 可以将内容看做一个整体,()中可以使用"|"来表示或关系。例如,(+86|0086) 表示这里可以是+86或者0086。
    
  • "^"和"$"

      通过在正则表达式的开始添加"^"以及末尾添加"$"来表示一个整体。若不使用它们,那么正则表达式只匹配某个字符串的部分内容是否符合格式规则,
      但使用它们,则要求字符串必须从头到尾都满足该格式规则。
      例如,^\w{ 8,10 }$ 表示整体字符串只能出现单词字符8-10个。
    

String正则相关API

  • matches方法

    matches()方法的参数要求我们传入一个用字符串描述的正则表达式,然后使用该正则表达式描述的字符串格式规则来匹配当前字符串,若满足那么该方法返回true。否则返回false。 例如:

      String emailRegEx =
               "^[a-zA-Z0-9_.-]+@([a-zA-Z0-9-]+\\.)+[a-zA-Z0-9]{2,4}$"; 
      String email = "bjliyi@tarena.com.cn";
      System.out.println(email.matches(emailRegEx));//true
    
  • split方法

    String[] split(String regex):参数要求传入一个用字符串描述的正则表达式,然后使用该正则表达式描述的字符串规则来匹配当前字符串,并按照满足的部分将字符串拆分。 例如:

      String str = "java,c#,php,javascript"; 
      String[] array = str.split(",");
      //[java,c#,php,javascript]
      System.out.println(Arrays.toString(array));
    
  • replaceAll方法

    String replaceAll(String regex,String replacement):参数要求传入一个用字符串描述的正则表达式和一个需要替换的字符串,然后使用该正则表达式描述的字符串规则来匹配当前字符串,并将满足的部分替换为需要替换的这个字符串。 例如:

      String str = "abc123bcd45ef6g7890";; 
      str = str.replaceAll("\\d+", "数字");
      System.out.println(str);//abc数字bcd数字ef数字g数字
    

Object

Object

  • Object

    Object类是java中所有类的顶级父类。若我们定义的一个类没有显式的使用extends继承某个类时,默认就是继承自Object的。

toString()方法

1. 如何重写toString方法

	1. 既然Object是所有类的顶级父类,那么在Object中定义的方法所有的类都具备。其中之一就是toStirng()方法。
	2. String toString():该方法java希望我们重写时返回一个字符串,这个字符串的原则为:用一个字符串来描述当前对象。
	3. Object实现了toString()方法,返回的是当前对象的“句柄”。
		格式为:类的完全限定名@hashcode。
		因为Object实现的toString()方法不具备什么实际开发意义,所以若我们需要在子类中使用该方法时通常我们会重写它。 

2. String类重写toString()

		public String toString(){
		    return this;
		}

	从源码中我们可以看到,String重写了Object的toString()方法,该方法直接将当前字符串对象自身返回。

equals()方法

  • equals方法

    boolean equals():该方法java希望我们重写时返回一个boolean值,表示两个对象间的内容比较是否一致。 Object已经实现了该方法,代码如下:

      public boolean equals (Object obj) {
          return (this == obj);
      }
    

    由此看出,实际上Object中重写该方法依旧使用"=="比较,所以当我们在子类中需要比较对象内容时就要重写该方法。

  • 如何重写equals方法

    重写equals方法应遵循几个规则:

      1. 任何对象与null比较都应返回false
      2. 两个对象不属于同一个类时应返回false
      3. 同一个对象equals比较应当恒等为true
    

    那么除此之外,两个对象在比较时,应根据具体的业务需求来自行决定对象的哪些属性相同时对象内容相同。

  • String重写equals()方法

    String重写了equals方法,作用是比较两个字符串对象中保存的字符序列是否完全一致。

  • equals与 == 的区别

      "=="是值比较,对于引用类型变量而言,该变量保存的是对象的地址,所以使用"=="比较时,意思为两个变量的地址是否相等,
      换句话说就是看两个变量引用的是否为同一个对象
      equals是内容比较,对于两个引用变量而言,是比较两个变量所引用的对象内容是否相同。
    
      举个例子,	就好像一对双胞胎,他们是两个独立的个体,是两个对象。
      所以那么用"=="比较是 false。但是因为他们“长得一样”,所以equals方法比较是true。
      我们也可以变相的理解为:"=="是判断是否为同一个,而"equals"是判断像不像。
    

包装类

包装类概述

用于解决基本类型不能参与面向对象开发的问题。
包装类可以将基本类型以对象的形式存在,从而
就具有了面向对象的相关特性。
数字类型包装类继承自Number类,可以在6中数字
类型之间转换。

8个基本类型包装类

  • Number及其主要方法

      上一节我们已经知道,除了Character与Boolean之外的其他包装类都是继承自Number的,这些包装类都有一个共性,描述的都是数字。
      那么我们来了解一下他们的父类:java.lang.Number
      Number是一个抽象类。本身不能实例化。Number 的子类必须提供将表示的数值转换为 byte、double、float、int、long 和 short 的方法
      比如:
      abstract double doubleValue() 以double形式返回指定的数值
      abstract int intValue() 以int形式返回指定的数值
      abstract float floatValue() 以float形式返回指定的数值
      剩下的抽象方法请参阅API文档:java.lang.Number 。
    
  • Integer常用功能

      java.lang.Integer是int的包装类,其每一个实例用于描述一个基本类型int的值。
      Integer有一个静态方法static int parseInt(String s)。
      该方法的作用是将一个描述整数的字符串解析为该整数,并用int形式返回。该方法可能会抛出NumberFormatException异常:
      当给定的字符串里边含有非整数字符时。
    
  • Double常用功能

      java.lang.Double是double的包装类,其每一个实例用于描述一个基本类型double的值。
      Double有一个静态方法static double parseDouble(String s)。
      该方法的作用是将一个描述小数的字符串解析为该小数,并用double形式返回。
      该方法可能会抛出NumberFormatException异常: 如果字符串不包含可解析的 double 值。
    
  • 自动装箱和拆箱操作

    java 5.0之后推出的一个新的特性

      public static void main(String[] args) {
      	/*
      	 * 自动拆装箱不是JVM认可的,而是编译器
      	 * 认可的。
      	 * 
      	 * 当编译器在编译下面代码时,会自动添加
      	 * 代码将基本类型转换为引用类型,所以在
      	 * 编译后的class文件中,下面代码的样子
      	 * 是:
      	 * Integer i = Integer.valueOf(1);
      	 */
      	Integer i = 1;
      	/*
      	 * 同样的,下面代码在编译后的class文件
      	 * 中的样子:
      	 * int ii = i.intValue();
      	 */
      	int ii = i;
      }
    

日期操作

Date及其常用API

  • JAVA 中的时间

    1. Java中的时间使用标准类库的Date类表示,是用距离一个固定时间点的毫秒数(可正可负,long类型)表达一个特定的时间点。
    2. 固定的时间点叫纪元(epoch),是UTC时间1970年 1月 1日 00:00:00。
    3. UTC(Universal Time Coordinated世界调整时间)与GMT(Greenwich Mean Time格林威治时间)一样,是一种具有实际目的的科学标准时间。
  • Date类简介

    java.util.Date 类封装日期及时间信息。

    Date类的大多数用于进行时间分量计算的方法已经被Calendar取代。

    因为Date的设计具有"千年虫"以及"时区"的问题,所以Date中的大部分方法已经不建议使用了,它们都被java.util.Calendar类所取代

  • setTime与getTime方法

     void setTime(long time):
    

    该方法用于为一个Date对象设置其需要表示的时间,该参数为一个long值,其含义是需要表示的这个时间点距离1970年1月1日 00:00:00之间的毫秒差。

     long getTime()
    

    该方法用于获取一个Date对象所表示的时间点,该返回值为一个long值,表示该时间点距离1970年1月1日 00:00:00之间的毫秒差。

  • Date 重写 toString方法

    Date重写了toString()方法,用一个字符串来描述当前Date对象所表示的时间。 格式如下:

     Mon Feb 17 15:36:55 CST 2014
    

    由此我们可以看出,实际上Date的toString()方法返回的字符串虽然很清晰的描述了时间,但是对于非英语地区来讲,该字符串不够友好,我们更希望按照特定地区表示时间的方式。比如我们更习惯以下的风格:

     2014-02-17 15:36:55 星期一
    

    那么有没有方式可以代替 Date的toString()方法来获取一个特定格式的字符串呢?答案是肯定的,java为我们提供了一个类,叫做SimpleDateFormat,该类就可以完成。

SimpleDateFormat

  • SimpleDateFormat简介

    SimpleDateFormat 是一个以与语言环境有关的方式来格式化和解析日期的具体类。它允许进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。

    简单的说,SimpleDateFormat就是根据一个特定的日期格式在字符串与Date之间相互转换。

  • 日期模式匹配字符串

    日期模式的匹配字符串如表所示。

    日期模式的匹配字符串

    例如: yyyy年MM月dd日--HH:mm:ss 可以匹配 2014年01月06日--13:22:41

  • 将Date格式化为String

    将Date格式化为String,我们需要使用SimpleDateFormat提供的方法:

     String format(Date d)
    

    例如:

     Date now = new Date();//默认实例化的Date表示当前系统时间
     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     String str = sdf.format(now);
     System.out.println(str);//2014-01-06 13:21:12
    
  • 将String解析为Date

    将String格式化为Date,我们需要使用SimpleDateFormat提供的方法:

      Date parse(String s)
    

    例如:

      String str = "2008年08月08日 12:22:46";
      SimpleDateFormat sdf 
                  = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
      Date date = sdf.parse(str);
      System.out.println(date);// Fri Aug 08 12:22:46 CST 2008
    

Calendar类

  • Calendar简介

    1. java.util.Calendar 类用于封装日历信息,其主要作用在于其方法可以对时间分量进行运算。
    2. Calendar是抽象类,其具体子类针对不同国家的日历系统,其中应用最广泛的是GregorianCalendar(格里高里历,即通用的阳历),对应世界上绝大多数国家/地区使用的标准日历系统。
  • getInstance()方法

    1. Calendar是抽象类,其提供了一个工厂方法:Calendar getInstance()。该方法可以根据当前系统所在地区获取一个适当的Calendar的子类实现。
    2. 在实际开发中,也推荐使用该方法来实例化Calendar的对象。
  • 设置日期及时间分量

    Calendar提供了一种通用的设置时间的方式:

      void set(int field,int value)
    

    该方法可以通过对不同的时间分量分别设置不同的值。Calendar对不同的时间分量提供了相应的常量,我们在使用set方法设置时,第一个参数就应当使用对应的常量作为时间分量。

      Calendar calendar = Calendar.getInstance();//创建出的Calendar表示当前系统时间
      //设置年为2008年
      calendar.set(Calendar.YEAR,2008);
      //设置月为5月
      calendar.set(Calendar.Month,4);//月份从0开始
      calendar.set(Calendar.Month,Calendar.MAY);//也可以使用常量来设置
      //设置日为30日
      caneldar.set(Calendar.DAY_OF_MONTH,30);
    
  • 获取时间分量对应的值

    Calendar提供了一种通用的获取时间分量的方式:

      int get(int field)
    

    该方法可以通过对不同的时间分量获取相应的值。Calendar对不同的时间分量提供了相应的常量,我们在使用get方法获取时,参数就应当使用对应的常量作为时间分量。

      Calendar calendar = Calendar.getInstance();
      int year = calendar.get(Calendar.YEAR);
      int month = calednar.get(Calendar.Month);
      int date = calendar.get(Calendar.DAY_OF_MONTH);
      //需要注意,月份要加1,因为月份是从0开始的
      System.out.println(year+"-"+(month+1)+"-"+date);//2014-1-4
    
  • getActualMaximum方法

    int getActualMaximum(int field)方法用于获取给定时间分量所允许的最大值

    例如:

    获取当前系统时间中当月的最后一天(日所允许的最大值)

      Calendar calendar = Calendar.getInstance();
      int max = calendar. getActualMaximum(Calendar.DAY_OF_MONTH);
      System.out.println("当前月的最后一天为:"+max+"日");//当前月的最后一天为31日
    
  • add方法

    Calendar还允许我们使用统一的方式来对某个时间分量的值进行计算。我们需要使用方法 void add(int field,int amount)。该方法可以为某个时间分量的值加上给定的值,若想减去给定的值,那么传入的值需要是负数即可。并且计算后会自动进行相应的进位,例如若当前为月底,那么加一天后,为下个月的月初,而月就会进位。

    例如: 当前系统时间为 2014-01-31日

      Calendar calendar = Calendar.getInstance();
      //计算明天(在日的基础上加一天)    
      calendar.add(Calendar.DAY_OF_YEAR,1);//当前Calendar表示的为2014-02-01,月进位了
    
  • setTime与getTime方法

    Calendar的void setTime(Date date),允许我们为Calendar设置Date对象所表示的时间。

    Calendar的 Date getTime(),允许我们获取一个使用Date对象描述的Calendar所表示的时间。

    例如:

      Calendar calendar = Calendar.getInstance();
      Date date = calendar.getTime();
      System.out.println(date);// Mon Feb 17 15:36:55 CST 2014
    
  • Calendar与Date之间的相互转换

      /*
       * 默认创建出来的是阳历历法:
       * GregorianCalendar
       * 
       * 默认就表示当前系统时间
       */
      Calendar calendar
      	= Calendar.getInstance();
      /*
       * Calendar的toString不能直观反映
       * 表示的具体日期
       */
      System.out.println(calendar);
    
      /*
       * 
       * Calendar提供了与Date之间互转的相关
       * 方法:
       * Calendar->Date
       * 
       * Date getTime()方法
       * 该方法会返回一个Date对象,该对象表示
       * 的时间就是当前Calendar表示的时间。
       */
      Date date = calendar.getTime();
      System.out.println(date);
    
      /*
       * Date -> Calendar
       * void setTime(Date date)
       * 该方法允许当前Calendar表示给定的
       * Date所表示的时间
       */
      calendar.setTime(date);
    

集合框架

Collection

java提供了一种可以存数一组数据的数据结构,其提供了丰富的方法,在实际开发中往往比数组使用的广泛。这种数据结构成为集合:Collection。
Collection是一个接口,其定义了集合的相关功能方法。
  • List和Set

      Collection派生出了两个子接口,一个是List另一个则是Set。
      List:称为可重复集,顾名思义,该集合中是允许存放重复元素的,那么何为重复元素?
      	 重复元素指的并非是同一个元素,而是指equals方法比较为true的元素。
      Set:称为不可重复集,所以,该集合中是不能将相同的元素存入集合两次,同List,
      	这里相同指的也是两个元素的equals比较结果为true。
    
  • 集合持有对象的引用

    集合中存储的都是引用类型的元素,那么引用类型变量实际上存储的是对象的“地址”,所以实际上集合只存储了元素对象在堆中的地址。而并不是将对象本身存入了集合中。

  • add()方法

    Collection定义了一个add方法用于向集合中添加新元素。

  • contains方法

    该方法会用于判断给定的元素是否被包含在集合中。若包含则返回true,否则返回false。

    这里需要注意的是,集合在判断元素是否被包含在集合中是使用元素的equals的比较结果。

    (o==null ? e==null : o.equals(e)) 其中e是集合中的元素。

  • size,clear,isEmpty方法

    size方法用于获取当前集合中的元素总数。该方法定义为:int size()

    clear方法用于清空集合。该方法定义为:void clear()

    isEmpty方法用于判断当前集合中是否不 包含元素。该方法定义为:boolean isEmpty()

  • addAll与containsAll方法

    addAll方法用于将给定集合中的所有元素添加到当前集合中

    containsAll方法用于判断当前集合是否包含给定集合中的所有元素,若包含则返回true。

Iterator 迭代器

Collection提供了一个遍历集合的通用方式,迭代器(Iterator)。
获取迭代器的方式是使用Collection定义的方法:

Iterator iterator()

迭代器Iterator是一个接口,集合在覆盖Collection的iterator()方法时提供了迭代器的实现。
Iterator提供了统一的遍历集合元素的方式。
  • hasNext与next方法

      迭代器用于遍历集合的两个主要方法:
    
      boolean hasNext():判断集合是否还有元素可以遍历。
      E next():返回迭代的下一个元素
    
      遍历集合应遵循“先问后取”的方式,也就是说,应当在确定hasNext()方法的返回值为true的情况下再通过next()方法取元素。
      由此可以看出,使用迭代器遍历集合是通过boolean值驱动的,所以它更适合使用while循环来遍历。
      例如:
    
      Collection<String> c = new HashSet<String>();
      c.add("java");        
      c.add("cpp");        
      c.add("php");
      c.add("c#");        
      c.add("objective-c");
      Iterator<String> it = c.iterator();
      while (it.hasNext()) {
          String str = it.next();
          System.out.println(str);
      } 
    
  • remove方法

      Collection c = new ArrayList();
      c.add("one");
      c.add("#");
      c.add("two");
      c.add("#");
      c.add("three");
      c.add("#");
      c.add("four");
    
      /*
       * Iterator iterator()
       * 该方法会返回一个Iterator的实现类:迭代器
       * 集合遍历元素使用的就是该统一的方式。
       * 迭代器遍历集合遵循:
       * 问,取,删。
       * 其中删除元素操作不是必须的。
       */
      Iterator it = c.iterator();
      /*
       * boolean hasNext()
       * 通过迭代器询问集合是否还有元素
       * 可以取出。
       */
      while(it.hasNext()){
      	/*
      	 * E next()
      	 * 从集合中取出下一个元素
      	 */
      	String str = (String)it.next();
      	System.out.println(str);
      	//从集合中删除"#"
      	if("#".equals(str)){
      		/*
      		 * 在使用迭代器遍历集合的过程中
      		 * 不能通过集合的方法修改集合元素
      		 * 数量,否则迭代器可能会抛出异常。
      		 */
      //		c.remove(str);
      		/*
      		 * void remove()
      		 * 迭代器提供的remove方法用于
      		 * 从集合中删除通过next()方法
      		 * 取出来的元素。
      		 */
      		it.remove();
      	}
      }
    
  • 增强for循环

    Java5.0之后推出了一个新的特性,增强for循环,也称为新循环。该循环不通用于传统循环的工作,其只用于便利集合或数组。 语法:

      for(元素类型 e : 集合或数组){
          循环体 
      }
    

    新循环并非新的语法,而是在编译过程中,编译器会将新循环转换为迭代器模式。所以新循环本质上是迭代器。 例如:

      Collection<String> c = new HashSet<String>();
      c.add("java");
      c.add("cpp");
      c.add("php");
      c.add("c#");
      c.add("objective-c");
      for (String str : c) {
          System.out.print(str.toUpperCase() + " ");
      } 
      // CPP PHP C# JAVA OBJECTIVE-C
    

泛型机制

  • 泛型在集合中的应用

    泛型是Java SE 5.0引入的特性,泛型的本质是参数化类型。在类、接口和方法的定义过程中,所操作的数据类型被传入的参数指定。 Java泛型机制广泛的应用在集合框架中。所有的集合类型都带有泛型参数,这样在创建集合时可以指定放入集合中的对象类型。Java编译器可以据此进行类型检查,这样可以减少代码在运行时出现错误的可能性。 我们来举个例子,比如ArrayList,其在定义时是这样的:

      public class ArrayList<E> {
          … … …                
          public boolean add(E e) {…};
          public E get(int index) {…};
      } 
    

    由此我们可以看出,再声明ArrayList时,类名的右侧有一个<E>。"<>"表示泛型,而其中可以使用数字字母下划线(数字不能的第一个字符)来表示泛型的名字。(通常我们使用一个大写字母来表示,当然这个不是规定。)这时,在类中声明的方法的参数,返回值类型可以被定义为泛型。这样在创建对象时可以将类型作为参数传递,此时,类定义所有的E将被替换成传入的参数。 例如:

      ArrayList<String> list = new ArrayList<String>();//泛型E在这里被指定为String类型
      list.add("One");//那么add方法的参数就被替换为String类型
      list.add(100);//这里就会出现编译错误,因为这里的参数应为String类型。
    

集合操作——线性表

List

List接口是Collection的子接口,用于定义线性表数据结构;可以将List理解为存放对象的数组,只不过其元素个数可以动态的增加或减少。并且List是可重复集
  • ArrayList和LinkedList

    List接口的两个常见实现类为ArrayList和LinkedList,分别用动态数组和链表的方式实现了List接口。

    可以认为ArrayList和LinkedList的方法在逻辑上完全一样,只是在性能上有一定的差别,ArrayList更适合于随机访问而LinkedList更适合于插入和删除;在性能要求不是特别苛刻的情形下可以忽略这个差别。

  • get与set方法

    List除了继承Collection定义的方法外,还根据其线性表的数据结构定义了一系列方法,其中最常用的就是基于下标的get和set方法。

    E get(int index):获取集合中指定下标对应的元素,下标从0开始。

    E set(int index, E elment):将给定的元素存入给定位置,并将原位置的元素返回。

    例如:

      List<String> list = new ArrayList<String>();
      list.add("java");        
      list.add("cpp");        
      list.add("php");
      list.add("c#");        
      list.add("objective-c");
      // get方法遍历List
      for (int i = 0; i < list.size(); i++) {
          System.out.println(list.get(i));
      } 
      String value = list.set(1, "c++");
      System.out.println(value); // cpp
      System.out.println(list); // [java, c++, php, c#, objective-c] 
      // 交换位置1和3上的元素
      list.set(1, list.set(3, list.get(1)));
      System.out.println(list); 
      // [java, c#, php, c++, objective-c]
    
  • 插入和删除

    List根据下标的操作还支持插入与删除操作:

      void add(int index,E element):
    

    将给定的元素插入到指定位置,原位置及后续元素都顺序向后移动。

      E remove(int index):
    

    删除给定位置的元素,并将被删除的元素返回。

    例如:

      List<String> list = new ArrayList<String>();
      list.add("java");
      list.add("c#");
      System.out.println(list); // [java, c#]
    
      list.add(1, "cpp");
      System.out.println(list); // [java, cpp, c#]
    
      list.remove(2);
      System.out.println(list); // [java, cpp]
    
  • subList方法

    List的subList方法用于获取子List。

    需要注意的是,subList获取的List与原List占有相同的存储空间,对子List的操作会影响的原List。

      List<E> subList(int fromIndex, int toIndex);
    

    fromIndex和toIndex是截取子List的首尾下标(前包括,后不包括) 。

    例如:

      List<Integer> list = new ArrayList<Integer>();
      for (int i = 0; i < 10; i++) {
          list.add(i);
      }
      System.out.println(list); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      List<Integer> subList = list.subList(3, 8);
      System.out.println(subList); // [3, 4, 5, 6, 7] 
      // subList获得的List和源List占有相同的数据空间
      for (int i = 0; i < subList.size(); i++) {
          subList.set(i, subList.get(i) * 10);
      }
      System.out.println(subList); // [30, 40, 50, 60, 70]
      System.out.println(list); // [0, 1, 2, 30, 40, 50, 60, 70, 8, 9] 
      // 可以用于删除连续元素list.subList(3, 8).clear();
      System.out.println(list);
    
  • List转换为数组

    List的toArray方法用于将集合转换为数组。但实际上该方法是在Collection中定义的,所以所有的集合都具备这个功能。

      Collection<String> c
      		= new ArrayList<String>();
    
      c.add("one");
      c.add("two");
      c.add("three");
      c.add("four");
    
      //Object[] array = c.toArray();
    
      String[] array
       = c.toArray(new String[c.size()]);
    
      System.out.println(array.length);
      System.out.println(Arrays.toString(array));
    
  • 数组转换为List

    Arrays类中提供了一个静态方法asList,使用该方法我们可以将一个数组转换为对应的List集合。 其方法定义为:

      static <T>List<T> asList<T… a>
    

    返回的List的集合元素类型由传入的数组的元素类型决定。

    需要注意的是,返回的集合我们不能对其增删元素,否则会抛出异常。并且对集合的元素进行的修改会影响数组对应的元素。 例如:

      String[] strArr = { "a", "b", "c" }; 
      List<String> list = Arrays.asList(strArr);
      System.out.println(list); // [a, b, c]
      // list.add("d"); // 会抛出UnsupportedOperationException 
      //     java.util.Arrays$ArrayList 
      System.out.println(list.getClass().getName());        
      List<String> list1 = new ArrayList<String>();
      list1.addAll(Arrays.asList(strArr)); 
    

    示例代码:

      	String[] array 
      			= {"one","two","three","four"};
    
      		for(int i=0;i<array.length;i++){
      			System.out.println(array[i]);
      		}
    
      		List<String> list
      			= Arrays.asList(array);
    
      		System.out.println(list);
    
      		list.set(0, "1");
      		System.out.println(list);
      		//修改集合元素就是修改数组对应元素
      		for(int i=0;i<array.length;i++){
      			System.out.println(array[i]);
      		}
      		//数组转换的集合不允许添加新元素
      //		list.add("five");
    
      		/*
      		 * 若有修改元素数量需求时,可以自行创建
      		 * 一个集合
      		 */
      		List<String> list1 
      			= new ArrayList<String>(list);
      //		list1.addAll(list);
      		list1.add("five");
      		System.out.println(list1);
    
      //		Set<String> set
      //			= new HashSet<String>(list1);
      //		System.out.println(set);
    

List排序

  • Collections.sort方法实现排序

    Collections是集合的工具类,它提供了很多便于我们操作集合的方法,其中就有用于集合排序的sort方法。该方法的定义为:

      void sort(List<T> list)
    

    其作用是对集合元素进行自然排序(按照元素的由小至大的顺序) 例如:

      List<Integer> list = new ArrayList<Integer>();
      Random r = new Random(1);
      for (int i = 0; i < 10; i++) {
          list.add(r.nextInt(100));
      } 
      System.out.println(list); // [85, 88, 47, 13, 54, 4, 34, 6, 78, 48]
      Collections.sort(list);
      System.out.println(list); // [4, 6, 13, 34, 47, 48, 54, 78, 85, 88]
    
  • Comparable

    通过上一节我们知道了如何对集合元素进行自然排序,但是要想对元素进行自然排序那么就必须要有一个必要条件,就是元素的大小。集合中存入的都是引用类型,是以对象的形式存在于内存中,那么对象是如何进行的大小比较呢?实际上,若想对某个集合的元素进行自然排序,该集合的元素有一个要求,就是这些元素必须是Comparable的子类。

    Comparable是一个接口,用于定义其子类是可以比较的。因为该接口有一个抽象方法:

      int compareTo(T t)
    
      所有子类都需要重写该方法来定义对象间的比较规则。该方法要求返回一个整数,这个整数不关心具体的值,而是关注取值范围。
      当返回值>0时,表示当前对象比参数给定的对象大。
      当返回值<0时,表示当前对象比参数给定的对象小。
      当返回值=0时,表示当前对象和参数给定的对象相等。
      例如:
      Class Cell implements Comparable<Cell>{
          int row;
          int col;
          public Cell(int row,int col){
              this.row = row;
              this.col = col;
          }
    
          public int compareTo(Cell c){
              //根据row比较大小
              return this.row - c.row;
          }
      }
    

    那么Collections的sort在进行排序时就会根据集合中元素的compareTo方法的返回值来判断大小从而进行自然排序。

      // Cell实现了Comparable接口,CompareTo方法逻辑为按照row值的大小排序
      List<Cell> cells = new ArrayList<Cell>();
      cells.add(new Cell(2, 3));
      cells.add(new Cell(5, 1));
      cells.add(new Cell(3, 2)); 
      Collections.sort(cells);
      System.out.println(cells); // [(2,3), (3,2), (5,1)]
    
  • Comparator

    一旦Java类实现了Comparable,其比较逻辑就已经确定;如果希望在排序的操作中临时指定比较规则,可以采用Comparator接口回调的方式。

    该接口要求实现类必须重写其定义的方法:

      int compare(T o1,T o2)
    

    该方法的返回值要求,若o1>o2则返回值应>0,若o1<o2则返回值应<0,若o1==o2则返回值应为0 例如:

      List<Cell> cells = new ArrayList<Cell>();
      cells.add(new Cell(2, 3));    
      cells.add(new Cell(5, 1));
      cells.add(new Cell(3, 2)); 
      // 按照col值的大小排序
      Collections.sort(cells, new Comparator<Cell>() {
          @Override
          public int compare(Cell o1, Cell o2) {
              return o1.col - o2.col;}
          });
      System.out.println(cells); // [(5,1), (3,2), (2,3)]
    

队列和栈

  • Queue

    队列(Queue)是常用的数据结构,可以将队列看成特殊的线性表,队列限制了对线性表的访问方式:只能从线性表的一端添加(offer)元素,从另一端取出(poll)元素。

    队列遵循先进先出(FIFO First Input First Output )的原则。

    JDK中提供了Queue接口,同时使得LinkedList实现了该接口(选择LinkedList实现Queue的原因在于Queue经常要进行插入和删除的操作,而LinkedList在这方面效率较高)。

      Queue提供了操作队列的相关方法,其主要方法如下:
      boolean offer(E e):将元素追加到队列末尾,若添加成功则返回true。
      E poll():从队首删除并返回该元素。
      E peek():返回队首元素,但是不删除。
    

    例如:

      Queue<String> queue = new LinkedList<String>();
      queue.offer("a");
      queue.offer("b");
      queue.offer("c");
      System.out.println(queue); // [a, b, c] 
      String str = queue.peek();
      System.out.println(str); // a 
      while (queue.size() > 0) {
          str = queue.poll();
          System.out.print(str + " "); // a b c
      } 
    
  • Deque

    Deque是Queue的子接口,定义了所谓“双端队列”即从队列的两端分别可以入队(offer)和出队(poll),LinkedList实现了该接口。

    如果将Deque限制为只能从一端入队和出队,则可实现“栈”(Stack)的数据结构,对于栈而言,入栈称之为push,出栈称之为pop。

    栈遵循先进后出(FILO First Input Last Output )的原则。

      Deque提供了操作栈的相关方法,其主要方法如下:
    
      void push(E e):将给定元素"压入"栈中。存入的元素会在栈首。即:栈的第一个元素
    
      E pop():将栈首元素删除并返回。
    

    例如:

      Deque<String> stack = new LinkedList<String>();
      stack.push("a");
      stack.push("b");
      stack.push("c");
      System.out.println(stack); // [c, b, a] 
      String str = stack.peek();
      System.out.println(str); // c 
      while (stack.size() > 0) {
          str = stack.pop();
          System.out.print(str + " "); // c b a
      } 
    

查询表

Map接口

  • Map 接口

      java提供了一组可以以键值对(key-value)的形式存储数据的数据结构,这种数据结构成为Map。
      我们可以把Map看成一个多行两列的表格,其中第一列存放key,第二列存放value。
      而每一行就相当于一组key-value对,表示一组数据。
      Map对存入的元素有一个要求,就是key不能重复,所谓不能重复指的是在Map中不能包含两个equals为true的key。
      Map对于key,value的类型没有严格要求,只要是引用类型均可。但是为了保证在使用时不会造成数据混乱,通常我们会使用泛型去约束key与value的类型。
    
  • put方法

    既然我们知道了Map在保存数据时实际上是存入了两部分信息的 ,key与value。那么我们来看看如何向Map中存入数据。

    Map提供了一个方法:

      V put(K k,V v)
    

    该方法的作用是将key-value对存入Map中,因为Map中不允许出现重复的key,所以若当次存入的key已经在Map中存在,则是替换value操作,而返回值则为被替换的元素。若此key不存在,那么返回值为null。

  • get方法

    我们学会了如何向Map中存入数据,那么我们再来看看如何获取数据。Map中获取数据的方式是给定Key获取对应的Value。

    Map提供了一个方法:

      V get(Object key)
    

    该方法的作用就是根据给定的key去查找Map中对应的value并返回,若当前Map中不包含给定的key,那么返回值为null。

  • containsKey方法

    Map中的containsKey方法用于检测当前Map中是否包含给定的key。其方法定义如下:

      boolean containsKey(Object key)
    

    若当前Map中包含给定的key(这里检查是否包含是根据key的equals比较结果为依据的。)则返回true。

HashMap

  • hash表原理

      HashMap是Map的一个常用的子类实现。其实使用散列算法实现的。
      HashMap内部维护着一个散列数组(就是一个存放元素的数组),我们称其为散列桶,
      而当我们向HashMap中存入一组键值对时,HashMap首先获取key这个对象的hashcode()方法的返回值,
      然后使用该值进行一个散列算法,得出一个数字,这个数字就是这组键值对要存入散列数组中的下标位置。
    
      那么得知了下标位置后,HashMap还会查看散列数组当前位置是否包含该元素。(这里要注意的是,散列数组中
      每个元素并非是直接存储键值对的,而是存入了一个链表,这个链表中的每个节点才是真实保存这组键值对
      的。)检查是否包含该元素时根据当前要存入的key在当前散列数组对应位置中的链表里是否已经包含这个key,
      若不包含则将这组键值对存入链表,否则就替换value。
    
      那么在获取元素时,HashMap同样先根据key的hashcode值进行散列算法,找到它在散列数组中的位置,
      然后遍历该位置的链表,找到该key所对应的value之后返回。
    
      看到这里可能有个疑问,链表中应该只能存入一个元素,那么HashMap是如何将key-value存入链表的
      某个节点的呢?实际上,HashMap会将每组键值对封装为一个Entry的实例,然后将该实例存入链表。
    
  • hashcode方法

    HashMap的存取是依赖于key的hashcode方法的返回值的,而hashcode方法实际上是在Object中定义的。其定义如下:

      int hashCode()
    
      重写一个类的hashcode()方法有以下注意事项:
      1、若一个类重写了equals方法,那么就应当重写hashcode()方法。
      2、若两个对象的equals方法比较为true,那么它们应当具有相同的hashcode值。
      3、对于同一个对象而言,在内容没有发生改变的情况下,多次调用hashCode()方法应当总是返回相同的值。
      4、对于两个对象equals比较为false的,并不要求其hashcode值一定不同,但是应尽量保证不同,这样可以提高散列表性能。
    
  • 装载因子及HashMap优化

      在散列表中有一下名词需要了解:
    
      Capacity:容量, hash表里bucket(桶)的数量, 也就是散列数组大小.
      Initial capacity:初始容量, 创建hash表的时 初始bucket的数量, 默认构建容量是16. 也可以使用特定容量.
      Size : 大小, 当前散列表中存储数据的数量.
      Load factor:加载因子, 默认值0.75(就是75%), 向散列表增加数据时如果 size/capacity 的值大于Load factor则发生扩容并且重新散列(rehash).
    
      那么当加载因子较小时候散列查找性能会提高, 同时也浪费了散列桶空间容量. 0.75是性能和空间相对平
      衡结果. 在创建散列表时候指定合理容量, 从而可以减少rehash提高性能。
    

有序Map

  • LinkedHashMap实现有序的Map

    Map 接口的哈希表和链表实现,具有可预知的迭代顺序。此实现与 HashMap 的不同之处在于, LinkedHashMap维护着一个双向循环链表。此链表定义了迭代顺序,该迭代顺序通常就是将存放元素的顺序。

    需要注意的是,如果在Map中重新存入以有的key,那么key的位置会不会发生改变,只是将value值替换。

示例代码

  • Map中的API

    示例代码:

      //Map可以分别制定Key与Value的类型
      Map<String,Integer> map
      	= new HashMap<String,Integer>();
    
      /*
       * V put(K k,V v)
       * 将指定的key与value存入Map中
       * 由于Map要求key不允许重复,所以
       * 若使用已有的key存入一个value,则会
       * 将该key原有对应的value值替换,并将
       * 被替换的value返回。若使用新的key,
       * 则返回值为NULL。
       */
      map.put("语文", 98);
      map.put("数学", 97);
      map.put("英语", 96);
      map.put("物理", 95);
      map.put("化学", 98);
      System.out.println(map);
    
      Integer num = map.put("语文", 99);
      System.out.println(map);
      System.out.println(num);
    
    
      /*
       * V get(K k)
       * 根据给定的key获取对应的value
       * 若给定的key在Map中不存在,则
       * 返回值为null
       */
      num = map.get("政治");
      System.out.println(num);
    
      num = map.get("英语");
      System.out.println(num);
    
      /*
       * V remove(K k)
       * 根据给定的key从Map中删除对应的
       * 这组键值对。而返回值则是该key对应
       * 的value
       */
      System.out.println("删除英语...");
      Integer old = map.remove("英语");
      System.out.println(map);
      System.out.println(old);
    

    示例代码:

      //Map可以分别制定Key与Value的类型
      	Map<String,Integer> map
      		= new HashMap<String,Integer>();				
      	map.put("语文", 98);
      	map.put("数学", 97);
      	map.put("英语", 96);
      	map.put("物理", 95);
      	map.put("化学", 98);
      	System.out.println(map);
    
      	boolean containsKey
      		= map.containsKey("语文");
      	System.out.println(containsKey);
    
  • 遍历Map

    示例代码:

      /**
       * Map的遍历
       * 遍历Map有三种方式:
       * 1:遍历所有的key
       * 2:遍历所有的key-value对
       * 3:遍历所有的value(相对而言不常用)
       * @author Administrator
       *
       */
      public class MapDemo3 {
      	public static void main(String[] args) {
      		Map<String,Integer> map
      			= new LinkedHashMap<String,Integer>();				
      		map.put("语文", 98);
      		map.put("数学", 97);
      		map.put("英语", 96);
      		map.put("物理", 95);
      		map.put("化学", 98);
    
      		/*
      		 * 遍历所有的key
      		 * 
      		 * Set<K> keySet()
      		 * 将当前Map中所有的key存入到一个Set
      		 * 集合中,并将该集合返回。
      		 */
      		Set<String> keySet = map.keySet();
      		for(String key : keySet){
      			System.out.println("key:"+key);
      		}
    
      		/*
      		 * 遍历每一组键值对
      		 * Entry是Map的内部类,其每一个实例用于
      		 * 表示一组键值对。有key,value两个主要
      		 * 属性组成。
      		 * 
      		 * Set<Entry<K,V>> entrySet()
      		 * 
      		 */
      		Set<Entry<String,Integer>> entrySet 
      			= map.entrySet();
    
      		for(Entry<String,Integer> entry:entrySet){
      			String key = entry.getKey();
      			Integer value = entry.getValue();
      			System.out.println(key+":"+value);
      		}
    
      		/*
      		 * Collection<V> values()
      		 * 将当前Map中所有的value存入到一个集合中
      		 * 然后返回该集合
      		 */
      		Collection<Integer> values
      			= map.values();
    
      		for(Integer value : values){
      			System.out.println("value:"+value);
      		}
    
      	}
      }
    

文件操作——File

创建File对象

java.io.File用于表示文件(目录),也就是说程序员可以通过File类在程序中操作硬盘上的文件和目录。

File类只用于表示文件(目录)的信息(名称、大小等),换句话说只能访问文件或目录的相关属性,不能对文件的内容进行访问。
  • File(pathname)

    File提供了较多的构造方法来创建实例,其中之一就是:

      File(String pathname)
    

    通过将给定路径名字符串转换成抽象路径名来创建一个新 File 实例

    提示:抽象路径应尽量使用相对路径,并且目录的层级分隔符不要直接写”/”或”\”,应使用File.separator这个常量表示,以避免不同系统带来的差异。

  • File(parent,child)

    File的另一个常用构造方法:

      File(File parent,String child)
    

    根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。

  • isFile() 方法

    File的isFile方法用于判断当前File对象表示的是否为一个文件

      boolean isFile()
    

    该方法若返回true,这表示File表示的是一个文件。

File表示文件信息

  • length方法

    File的length方法用于返回由此抽象路径名表示的文件的长度,其定义为:

      long length() 
    

    该方法返回的long值表示该文件所占用的字节量。

  • exists方法

    File的exists方法用于测试此抽象路径名表示的文件或目录是否存在,其方法定义:

      boolean exists() 
    

    若该File表示的文件或目录存在则返回true,否则返回false。

  • createNewFile方法

    File的createNewFile方法用于当且仅当不存在具有此抽象路径名指定的名称的文件时,原子地创建由此抽象路径名指定的一个新的空文件。 其方法定义:

      boolean createNewFile()
    

    返回值:如果指定的文件不存在并成功地创建,则返回 true;如果指定的文件已经存在,则返回 false 。

  • delete方法

    File的delete方法用于删除此抽象路径名表示的文件或目录。 其方法定义:

      boolean delete()
    

    返回值:当且仅当成功删除文件或目录时,返回 true;否则返回 false。

    需要注意的是,若此File对象所表示的是一个目录时,在删除时需要保证此为空目录才可以成功删除(目录中不能含有任何子项)。

  • isDirectory()

    File的isDirectory方法用于判断当前File对象表示的是否为一个目录

      boolean isDirectory()
    

    返回值:若File对象表示的是一个目录,则返回true

File表示目录信息

  • mkdir方法

    File的mkdir方法用于创建此抽象路径名指定的目录。其方法定义:

      boolean mkdir()
    

    返回值:当且仅当已创建目录时,返回 true;否则返回 false

  • mkdirs方法

    File的mkdirs方法用于创建此抽象路径名指定的目录,包括所有必需但不存在的父目录。注意,此操作失败时也可能已经成功地创建了一部分必需的父目录。其方法定义:

      boolean mkdirs()
    

    返回值:当且仅当已创建目录以及所有必需的父目录时,返回 true;否则返回 false

  • delete方法

    前面我们介绍了File的delete方法是用于删除此抽象路径名表示的文件或目录。在此强调,在删除目录时要特别注意:需要保证此为空目录才可以成功删除(目录中不能含有任何子项)。

  • listFiles方法

    File的listFiles方法用于返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。其方法定义:

      File[] listFiles() 
    

    返回值:抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件和目录。如果目录为空,那么数组也将为空。如果抽象路径名不表示一个目录,或者发生 I/O 错误,则返回 null。

  • FileFilter接口

    通过listFiles方法我们可以获取一个目录下的所有子项,但有些时候我们并不希望获取全部子项,而是想获取部分满足我们实际需求的子项时,我们可以使用File的重载方法:

      File[] listFiles(FileFilter  filter)
    

    这里我们看到,该重载方法 要求我们传入一个参数,其类型是FileFilter。什么是FileFilter呢? FileFilter是用于抽象路径名的过滤器,说白了就是定义一个规律规则,那么结合listFiles方法,我们就可以将满足此过滤规则的子项返回,其他则忽略。

    FileFilter是一个接口,所以当我们需要定义某种过滤规则时,我们可以定义一个类来实现这个接口,而此接口的实例可传递给 File 类的 listFiles(FileFilter) 方法。

    例如:

      File[] list = dir.listFiles(new FileFilter() {
          @Override
          public boolean accept(File pathname) {
              return pathname.getName().startsWith(".");
          }
      }); 
    

    该方法的参数FileFilter实例的accept方法并进行过滤,listFiles方法会将所有accept方法返回true的子项保留并返回。这个例子里我们会将dir中子项的名字以"."开头的返回。

示例代码

  • 访问File

    使用File可以:

  • 1:访问文件或目录的属性信息

  • 2:操作文件或目录(创建,删除)

  • 3:查看目录中的子项

    示例代码:

     File file = new File(
     	"."+File.separator+"test.txt"
     );
     //获取文件名
     String name = file.getName();
     System.out.println("name:"+name);
    
     //文件大小(字节)
     long length = file.length();
     System.out.println("length:"+length+"字节");
     //最后修改时间
     long lm = file.lastModified();
    
     boolean cr = file.canRead();
    
     boolean cw = file.canWrite();
    
     boolean ih = file.isHidden();
    

    使用File创建文件

     /*
      * 在当前项目根目录下创建文件demo.txt
      */
     File file = new File("demo.txt");
     /*
      * boolean exists()
      * 判断当前File表示的文件或目录是否真实
      * 存在。
      */
     if(!file.exists()){
     	System.out.println("不存在!");
     	file.createNewFile();
     	System.out.println("创建完毕!");
     }
    

    使用File删除现有文件

     /*
      * 删除项目根目录下的demo.txt
      */
     File file = new File("demo.txt");
     if(file.exists()){
     	//删除File表示的文件
     	file.delete();
     	System.out.println("删除完毕!");
     }
    

    使用File创建一个目录

     /*
      * 在当前项目根目录下创建一个叫demo的目录
      */
     File dir = new File("demo");
    
     if(!dir.exists()){
     	//创建目录
     	dir.mkdir();
     	System.out.println("创建完毕!");
     }
    

    使用File创建一个多级目录

     /*
      * 在当前目录下创建目录:a/b/c/d/e/f
      */
     File dir = new File(
     	"a"+File.separator+
     	"b"+File.separator+
     	"c"+File.separator+
     	"d"+File.separator+
     	"e"+File.separator+
     	"f");
     if(!dir.exists()){
     	/*
     	 * mkdirs会在创建当前目录的同时将
     	 * 所有不存在的父目录全部自动创建
     	 */
     	dir.mkdirs();
     	System.out.println("创建完毕!");
     }
    

    删除一个目录

     File dir = new File("demo");
     if(dir.exists()){
     	/*
     	 * 若当前目录中含有子项,该目录不能
     	 * 被删除。
     	 */
     	dir.delete();
     	System.out.println("删除完毕!");
     }
    

    使用File获取其表示的目录中的所有子项,一个目录中的子项无非还是文件或目录

     /*
      * 获取当前目录下的所有子项
      */
     File dir = new File(".");
    
     /*
      * File[] listFiles()
      * 将当前目录中所有子项(若干File对象表示)
      * 存入一个数组后返回
      */
     File[] subs = dir.listFiles();
     for(File sub : subs){
     	/*
     	 * boolena isFile()
     	 * 判断当前File对象表示的是否为一个文件
     	 */
     	if(sub.isFile()){
     		System.out.print("文件:");
     	}
     	if(sub.isDirectory()){
     		System.out.print("目录:");
     	}
     	System.out.println(sub.getName());
     }
    

    File的listFiles方法有一个重载,允许我们指定一个文件过滤器,然后将File表示的目录下,满足过滤器要求的子项获取回来。

     File dir = new File(".");
    
     /*
      * 获取当前目录下所有名字以"."开头的子项
      * 
      * FileFilter是一个接口,有一个抽象方法:
      * accept,该方法的作用是定义过滤条件
      */
     FileFilter filter = new FileFilter(){
     	public boolean accept(File file) {
     		String name = file.getName();
     		System.out.println("正在过滤:"+name);
     		return name.startsWith(".");
     	}			
     };
    
    
     File[] subs = dir.listFiles(filter);
     for(File sub : subs){
     	System.out.println(sub.getName());
     }
    

    将指定的File表示的文件或目录删除

     public static void main(String[] args) {
     	File dir = new File("a");
     	delete(dir);
     }
     /**
      * 删除指定File表示的文件或目录
      * @param file
      * 递归
      */
     public static void delete(File file){
     	if(file.isDirectory()){
     		//将当前目录下的所有子项先删除
     		for(File sub : file.listFiles()){
     			delete(sub);
     		}
     	}
     	file.delete();
     }
    

文件操作——RandomAccessFile

创建对象

  • 简介

    Java提供了一个可以对文件随机访问的操作,访问包括读和写操作。该类名为RandomAccessFile。该类的读写是基于指针的操作。

  • 只读模式

    RandomAccessFile在对文件进行随机访问操作时有两个模式,分别为只读模式(只读取文件数据),和读写模式(对文件数据进行读写)。

    只读模式:

    在创建RandomAccessFile时,其提供的构造方法要求我们传入访问模式:

      RandomAccessFile(File file,String mode)
    
      RandomAccessFile(String filename,String mode)
    

    其中构造方法的第一个参数是需要访问的文件,而第二个参数则是访问模式:

      r”:字符串”r”表示对该文件的访问是只读的。
    
  • 读写模式

    创建一个基于文件访问的读写模式的RandomAccessFile我们只需要在第二个参数中传入”rw”即可。

      RandomAccessFile raf = new RandomAccessFile(file,”rw”);
    

    那么这时在使用RandomAccessFile对该文件的访问就是又可读又可写的。

字节数据读写操作

  • write(int d)方法

    RandomAccessFile提供了一个可以向文件中写出字节的方法:

      void write(int d)
    

    该方法会根据当前指针所在位置处写入一个字节,是将参数int的”低8位”写出。

  • read()方法

    RandomAccessFile提供了一个可以从文件中读取字节的方法:

      int read() 
    

    该方法会从RandomAccessFile当前指针位置读取一个byte(8位) 填充到int的低八位, 高24位为0, 返回值范围正数: 0~255, 如果返回-1表示读取到了文件末尾EOF(EOF:End Of File)! 每次读取后自动移动文件指针, 准备下次读取。

  • write(byte[] d)方法

    RandomAccessFile提供了一个可以向文件中写出一组字节的方法:

      void write(byte[] d)
    

    该方法会根据当前指针所在位置处连续写出给定数组中的所有字节,与该方法相似的还有一个常用方法:

      void write(byte[] d,int offset,int len)
    

    该方法会根据当前指针所在位置处连续写出给定数组中的部分字节,这个部分是从数组的offset处开始,连续len个字节。

  • read(byte[] d)方法

    RandomAccessFile提供了一个可以从文件中批量读取字节的方法:

      int read(byte[] b)
    

    该方法会从文件中尝试最多读取给定数组的总长度的字节量,并从给定的字节数组第一个位置开始,将读取到的字节顺序存放至数组中,返回值为实际读取到的字节量 。

  • close方法

    RandomAccessFile在对文件访问的操作全部结束后,要调用close()方法来释放与其关联的所有系统资源。

      void close()    
    

    例如:

      RandomAccessFile raf = new RandomAccessFile(file,”rw”);
      …..//读写操作 
      raf.close();//访问完毕后要关闭以释放系统资源。 
    

文件指针操作

  • getFilePointer方法

    RandomAccessFile的读写操作都是基于指针的,也就是说总是在指针当前所指向的位置进行读写操作。

    RandomAccessFile提供了一个可以获取当前指针位置的方法:

      long getFilePointer()
    

    RandomAccessFile在创建时默认指向文件开始(第一个字节),通过getFilePointer方法获取指针位置时值是"0"。

    例如:

      RandomAccessFile raf = new RandomAccessFile(file,”rw”);
      System.out.println(raf.getFilePointer());//0
      raf.write(‘A’);//写出一个字节后,指针自动向后移动到下一个字节位置
      System.out.println(raf.getFilePointer());//1
      raf.writeInt(3);
      System.out.println(raf.getFilePointer());//5
      raf.close(); 
    
  • seek方法

    RandomAccessFile的提供了一个方法用于移动指针位置。

      void seek(long pos)
    

    使用该方法可以移动指针到指定位置。

    例如:

      RandomAccessFile raf = new RandomAccessFile(file,”rw”);
      System.out.println(raf.getFilePointer());//0
      raf.write(‘A’);//指针位置1
      raf.writeInt(3);//指针位置5
      raf.seek(0);//将指针移动到文件开始处(第一个字节的位置)
      System.out.println(raf.getFilePointer());//0
      raf.close(); 
    
  • skipBytes方法

    RandomAccessFile的提供了一个方法可以尝试跳过输入的 n 个字节以丢弃跳过的字节,方法定义为:

      int skipBytes(int n)
    

    该方法可能跳过一些较少数量的字节(可能包括零)。这可能由任意数量的条件引起;在跳过n个字节之前已到达文件的末尾只是其中的一种可能。此方法不抛出 EOFException。返回跳过的实际字节数。如果 n 为负数,则不跳过任何字节。

示例代码

  • 对项目根目录下的demo.dat文件进行读写操作

      RandomAccessFile raf
      	= new RandomAccessFile(
      		"demo.dat","rw"	
      	);
    
      //int i = 1;
      /*
       * void write(int i)
       * 向文件中写出一个字节,写出的是给定的
       * int值对应的二进制中的"低八位"
       *                            vvvvvvvv
       * 00000000 00000000 00000000 00000001
       * 
       * 00000000 00000000 00000001 00000001
       */
      raf.write(257); // 文件中只有1
      /*
       * 关闭释放资源
       */
      raf.close();
    
  • 读取字节

      RandomAccessFile raf
      	= new RandomAccessFile(
      		"demo.dat","r"	
      	);
    
      /*
       * byte read()
       * 该方法会读取一个字节,并将该字节转换为
       * 一个int值保存。该int值只有"低八位"有效
       * 若该int值表示的数字为-1,则表示读取到了
       * 文件末尾。
       * 00000000 00000000 00000000 11111111
       */
      int d = raf.read();
      System.out.println(d);
    
      raf.close();
    
  • RandomAccessFile读写基本类型数据以及基于指针的读写操作原理

      RandomAccessFile raf
      	= new RandomAccessFile(
      		"test.dat","rw"	
      	);
    
      long pos = raf.getFilePointer();
      System.out.println("pos:"+pos);
    
      //向文件中写入int最大值
      int max = Integer.MAX_VALUE;
      /*                           
       *                            vvvvvvvv
       * 01111111 11111111 11111111 11111111
       * 
       */
    
      raf.write(max>>>24);		
      raf.write(max>>>16);
      raf.write(max>>>8);
      raf.write(max);
      System.out.println("pos:"+raf.getFilePointer());
    
    
      raf.writeInt(max);
      System.out.println("pos:"+raf.getFilePointer());
    
    
      raf.writeLong(123L);
      System.out.println("pos:"+raf.getFilePointer());
    
    
      raf.writeDouble(123.123);
      System.out.println("pos:"+raf.getFilePointer());
    
    
      /*
       * void seek(long pos)
       * 移动指针到指定的位置
       */
      raf.seek(0);
      System.out.println("seek到0");
      System.out.println("pos:"+raf.getFilePointer());
      //EOF (end of file)
      int i = raf.readInt();
      System.out.println(i);
      System.out.println("pos:"+raf.getFilePointer());
    
      //读double
      raf.seek(16);
      double d = raf.readDouble();
      System.out.println(d);
      System.out.println("pos:"+raf.getFilePointer());
      raf.close();
    
  • 使用RAF复制文件

      /*
       * 1:创建一个RAF读取原文件
       * 2:再创建一个RAF用于向目标文件中写
       * 3:循环从原文件中读取每一个字节,然后
       *   将该字节的内容写入到目标文件中,直到
       *   读取到原文件的末尾
       */
      //1
      RandomAccessFile src
      	= new RandomAccessFile(
      		"music.mp3","r"
      	);
      //2
      RandomAccessFile desc
      	= new RandomAccessFile(
      		"music_copy.mp3","rw"	
      	);
    
      long start = System.currentTimeMillis();
      //3
      int d = -1;
      while((d=src.read())!=-1){
      	desc.write(d);
      }
      long end = System.currentTimeMillis();
    
    
      System.out.println("复制完毕!耗时:"+(end-start)+"ms");
      src.close();
      desc.close();
    
  • 基于缓存的文件复制

      RandomAccessFile src
      	= new RandomAccessFile(
      		"music.mp3","r"	
      	);
    
      RandomAccessFile desc
      	= new RandomAccessFile(
      		"music_copy2.mp3","rw"	
      	);
    
      /*
       * int read(byte[] d)
       * 一次性读取给定的字节数组长度的字节量
       * 并存入给定的数组中,而返回值为实际读取
       * 到得字节量
       * 若读取到了文件末尾则返回-1
       */
      int len = -1;
      byte[] buf = new byte[1024*10];
    
      long start = System.currentTimeMillis();
    
      while((len = src.read(buf))!=-1){
      	/*
      	 * void write(byte[] d)
      	 * 一次性将给定的字节数组中的所有字节
      	 * 写出去
      	 * void write(byte[] d,int offset,int len)
      	 * 将给定数组中offset指定的位置处开始的连续
      	 * len个字节写出
      	 * 
      	 */
      	desc.write(buf,0,len);	
      }
    
      long end = System.currentTimeMillis();
      System.out.println("复制完毕!耗时:"+(end-start)+"毫秒");
    
      src.close();
      desc.close();
    

基本IO操作

InputStream与OutputStream

  • 输入与输出

    我们编写的程序除了自身会定义一些数据信息外,经常还会引用外界的数据,或是将自身的数据发送到外界。比如,我们编写的程序想读取一个文本文件,又或者我们想将程序中的某些数据写入到一个文件中。这时我们就要使用输入与输出。

    什么是输入:输入是一个从外界进入到程序的方向,通常我们需要“读取”外界的数据时,使用输入。所以输入是用来读取数据的。

    什么是输出:输出是一个从程序发送到外界的方向,通常我们需要”写出”数据到外界时,使用输出。所以输出是用来写出数据的。

  • 节点流与处理流

    按照流是否直接与特定的地方 (如磁盘、内存、设备等) 相连,分为节点流和处理流两类。

    1. 节点流:可以从或向一个特定的地方(节点)读写数据。
    2. 处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。

    处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。

  • InputStream与OutputStream常用方法

    InputStream是所有字节输入流的父类,其定义了基础的读取方法,常用的方法如下:

      int read()
    

    读取一个字节,以int形式返回,该int值的”低八位”有效,若返回值为-1则表示EOF。

      int read(byte[] d)
    

    尝试最多读取给定数组的length个字节并存入该数组,返回值为实际读取到的字节量。

    OutputStream是所有字节输出流的父类,其定义了基础的写出方法,常用的方法如下:

      void write(int d) 
    

    写出一个字节,写的是给定的int的”低八位”

      void write(byte[] d)
    

    将给定的字节数组中的所有字节全部写出

文件流

  • 创建FOS对象(重写模式)

    FileOutputStream是文件的字节输出流,我们使用该流可以以字节为单位将数据写入文件。

    构造方法:

      FileOutputStream(File file)
    

    创建一个向指定 File 对象表示的文件中写入数据的文件输出流。

    例如:

      FIle file = new File("demo.dat");
      FileOutputStream fos = new FileOutputStream(file); 
    

    构造方法:

      FileOutputStream(String filename):
    

    创建一个向具有指定名称的文件中写入数据的输出文 件流。

    例如:

      FileOutputStream fos = new FileOutputStream("demo.dat"); 
    

    这里需要注意,若指定的文件已经包含内容,那么当使用FOS对其写入数据时,会将该文件中原有数据全部清除。

  • 创建FOS对象(追加模式)

    通过上一节的构造方法创建的FOS对文件进行写操作时会覆盖文件中原有数据。若想在文件的原有数据之后追加新数据则需要以下构造方法创建FOS

    构造方法:

      FileOutputStream(File file,boolean append)
    

    创建一个向指定 File 对象表示的文件中写入数据的文件输出流。

    例如:

      File file = new File("demo.dat");
      FileOutputStream fos = new FileOutputStream(file,true);
    

    构造方法:

      FileOutputStream(String filename,boolean append):
    

    创建一个向具有指定名称的文件中写入数据的输出文 件流。

    例如:

      FileOutputStream fos = new FileOutputStream("demo.dat",true);
    

    以上两个构造方法中,第二个参数若为true,那么通过该FOS写出的数据都是在文件末尾追加的。

  • 创建FIS对象

    FileInputStream是文件的字节输入流,我们使用该流可以以字节为单位读取文件内容。

    FileInputStream有两个常用的构造方法:

      FileInputStream(File file):
    

    创建用于读取给定的File对象所表示的文件FIS

    例如:

      File file = new File("demo.dat");
      FileInputStream fis 
      	= new FileInputStream(file);//创建一个用于读取demo.dat文件的输入流
    

    另一个构造方法:

      FileInputStream(String name):
    

    创建用于读取给定的文件系统中的路径名name所指定的文件的FIS

    例如

      FileInputStream fis
      	//创建一个用于读取demo.dat文件的输入流
      	= new FileInputStream("demo");
    
  • read()和write(int d)方法

    FileInputStream继承自InputStream,其提供了以字节为单位读取文件数据的方法read。

      int read()
    

    从此输入流中读取一个数据字节,若返回-1则表示EOF(End Of File)

    FileOutputStream继承自OutputStream,其提供了以字节为单位向文件写数据的方法write。

      void write(int d)
    

    将指定字节写入此文件输出流。,这里只写给定的int值的”低八位”

    例如

      FileOutputStream fos = new FileOutputStream("demo.dat");
      fos.write('A');//这里要注意,char占用2个字节,但这里只写入了1个字节。
    
  • read(byte[] d)和write(byte[] d)方法

    FileInputStream也支持批量读取字节数据的方法:

      int read(byte[] b)
    

    从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中 。

    FileOutputStream也支持批量写出字节数据的方法:

      void write(byte[] d)
    

    将 b.length 个字节从指定 byte 数组写入此文件输出流中。

    例如:

      FileOutputStream fos = new FileOutputStream("demo.txt");
      byte[] data = "HelloWorld".getBytes();
      fos.write(data);//会将HelloWorld的所有字节写入文件。 
    

    将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流的方法:

      void write(byte[] d,int offset,int len)
    

    例如:

      FileOutputStream fos = new FileOutputStream("demo.txt");
      byte[] data = "HelloWorld".getBytes();
      fos.write(data,5,5);//只会将world这5个字节写入文件。 
    

缓冲流

  • BOS基本工作原理

    与缓冲输入流相似,在向硬件设备做写出操作时,增大写出次数无疑也会降低写出效率,为此我们可以使用缓冲输出流来一次性批量写出若干数据减少写出次数来提高写 出效率。

    BufferedOutputStream缓冲输出流内部也维护着一个缓冲区,每当我们向该流写数据时,都会先将数据存入缓冲区,当缓冲区已满时,缓冲流会将数据一次性全部写出。

  • BOS实现写出缓冲

    实现写出缓冲:

      public void testBos()throws Exception {
          FileOutputStream fos = new FileOutputStream("demo.dat");
          //创建缓冲字节输出流
          BufferedOutputStream bos
                  = new BufferedOutputStream(fos);
      //所有字节被存入缓冲区,等待一次性写出
          bos.write("helloworld".getBytes());
          //关闭流之前,缓冲输出流会将缓冲区内容一次性写出
          bos.close();
      }
    
  • BOS的flush方法

    使用缓冲输出流可以提高写出效率,但是这也存在着一个问题,就是写出数据缺乏即时性。有时我们需要在执行完某些写出操作后,就希望将这些数据确实写出,而非在缓冲区中保存直到缓冲区满后才写出。这时我们可以使用缓冲流的一个方法flush。

      void flush()
    

    清空缓冲区,将缓冲区中的数据强制写出。

    例如:

      BufferedOutputStream bos
                  = new BufferedOutputStream(
                      new FileOutputStream("demo.dat")
                  );
      bos.write('a');//并没有向磁盘写出,而是写入到了BOS的缓存中
      bos.flush();//强制将缓存中的数据一次性写出,这时‘a’才会被写入磁盘
      bos.close();//实际上,close()方法在变比缓冲流前也调用了flush()
    
  • BIS基本工作原理

    在读取数据时若以字节为单位读取数据,会导致读取次数过于频繁从而大大的降低读取效率。为此我们可以通过提高一次读取的字节数量减少读写次数来提高读取的效率。

    BufferedInputStream是缓冲字节输入流。其内部维护着一个缓冲区(字节数组),使用该流在读取一个字节时,该流会尽可能多的一次性读取若干字节并存入缓冲区,然后逐一的将字节返回,直到缓冲区中的数据被全部读取完毕,会再次读取若干字节从而反复。这样就减少了读取的次数,从而提高了读取效率。

    BIS是一个处理流,该流为我们提供了缓冲功能。

  • BIS实现输入缓冲

    使用缓冲流来实现文件复制:

      FileInputStream fis = new FileInputStream("java.zip");
      BufferedInputStream bis = new BufferedInputStream(fis);
      FileOutputStream fos = new FileOutputStream("copy_java.zip");
      BufferedOutputStream bos = new BufferedOutputStream(fos);
      int d = -1;
      while((d = bis.read())!=-1){
          bos.write(d);
      }
      bis.close();//读写完毕后要关闭流,只需要关闭最外层的流即可
      bos.close();
    

对象流

  • 对象序列化概念

    对象是存在于内存中的。有时候我们需要将对象保存到硬盘上,又有时我们需要将对象传输到另一台计算机上等等这样的操作。这时我们需要将对象转换为一个字节序列,而这个过程就称为对象序列化。相反,我们有这样一个字节序列需要将其转换为对应的对象,这个过程就称为对象的反序列化。

  • 使用OOS实现对象序列化

    ObjectOutputStream是用来对对象进行序列化的输出流。

    其实现对象序列化的方法为:

      void writeObject(Object o)
    

    该方法可以将给定的对象转换为一个字节序列后写出。

    例如:

      Emp emp = new Emp("张三",12,"男");
      FileOutputStream fos = new FileOutputStream("Emp.obj");
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      oos.writeObject(emp);//将emp对象序列化后写入文件
      oos.close();
    
  • 使用OIS实现对象反序列化

    ObjectInputStream是用来对对象进行反序列化的输入流。

    其实现对象反序列化的方法为:

      Object readObject()
    

    该方法可以从流中读取字节并转换为对应的对象。

    例如:

      FileInputStream fis = new FileInputStream("Emp.obj");
      ObjectInputStream ois = new ObjectInputStream(fis);
      Emp emp = (Emp)ois.readObject();//将Emp对象从文件中读取并反序列
      ....
      ois.close();
    
  • Serializable接口

    ObjectOutputStream在对对象进行序列化时有一个要求,就是需要序列化的对象所属的类必须实现Serializable接口。

    实现该接口不需要重写任何方法。其只是作为可序列化的标志。

    通常实现该接口的类需要提供一个常量serialVersionUID,表明该类的版本。若不显示的声明,在对象序列化时也会根据当前类的各个方面计算该类的默认serialVersionUID,但不同平台编译器实现有所不同,所以若向跨平台,都应显示的声明版本号。

    如果声明的类序列化存到硬盘上面,之后随着需求的变化更改了类别的属性(增加或减少或改名),那么当反序列化时,就会出现InvalidClassException,这样就会造成不兼容性的问题。 但当serialVersionUID相同时,它就会将不一样的field以type的预设值反序列化,可避开不兼容性问题。

    例如:

      public class Emp implements Serializable{
          private static final long serialVersionUID = 1L;
          private String name;
          private int age;
          private String gender;
          //getter and setter and other
          ...
      }
    
  • transient关键字

    对象在序列化后得到的字节序列往往比较大,有时我们在对一个对象进行序列化时可以忽略某些不必要的属性,从而对序列化后得到的字节序列”瘦身”。

    关键字 transient

    被该关键字修饰的属性在序列化时其值将被忽略

    例如:

      public class Emp implements Serializable{
          private static final long serialVersionUID = 1L;
          private String name;
          private transient int age;//该属性在序列化时会被忽略
          private String gender;
          //getter and setter and other
          ...
      }
    

文件数据IO操作

Reader和Writer

  • 字符流原理

    Reader是所有字符输入流的父类,而Writer是所有字符输出流的父类。字符流是以字符(char)为单位读写数据的。一次处理一个unicode。字符流都是高级流,其底层都是依靠字节流进行读写数据的,所以底层仍然是基于字节读写数据的。

  • 常用方法

    Reader的常用方法:

      int read()
    

    读取一个字符,返回的int”值低16”位有效。

      int read(char[] chs)
    

    从该流中读取一个字符数组length个字符并存入该数组,返回值为实际读取到的字符量。

    Writer的常用方法:

      void write(int c)
    

    写出一个字符,写出给定int值”低16”位表示的字符。

      void write(char[] chs)
    

    将给定字符数组中所有字符写出。

      void write(String str)
    

    将给定的字符串写出

      void write(char[] chs,int offset,int len):
    

    将给定的字符数组中从offset处开始连续的len个字符写出

转换流

  • 字符转换流原理

    InputStreamReader:字符输入流, 使用该流可以设置字符集,并按照指定的字符集从流中按照该编码将字节数据转换为字符并读取。

    OutputStreamWriter:字符输出流,使用该流可以设置字符集,并按照指定的字符集将字符转换为对应字节后通过该流写出。

  • 指定字符编码

    InputStreamReader的构造方法允许我们设置字符集:

      InputStreamReader(InputStream in,String charsetName)
    

    基于给定的字节输入流以及字符编码创建ISR

      InputStreamReader(InputStream in)
    

    该构造方法会根据系统默认字符集创建ISR

    OutputStreamWriter:的构造方法:

      OutputStreamWriter(OutputStream out,String charsetName)
    

    基于给定的字节输出流以及字符编码创建OSW

      OutputStreamWriter(OutputStream out) 
    

    该构造方法会根据系统默认字符集创建OSW

  • 使用OutputStreamWriter

      ...
      public void testOutput() throws IOException{
          FileOutputStream fos 
              = new FileOutputStream("demo.txt");
          OutputStreamWriter writer
              //这里使用的字符编码为UTF-8
              = new OutputStreamWriter(fos,"UTF-8");        
          String str = "大家好!";//UTF-8中文为3个字节,英文符号占1个字节
          writer.write(str);//写出后该文件大小应该为10字节
          writer.close();
      }
      ... 
    
  • 使用InputStreamReader

      ...
      public void testInput() throws IOException{
          FileInputStream fis 
              = new FileInputStream("demo.txt");
          /*
          *  这里设置了字符编码为GBK
          *  之后再通过ISR读取demo.txt文件时
          *  就使用GBK编码读取字符了
          */
          InputStreamReader reader
              = new InputStreamReader(fis,"GBK");
          int c = -1;
          while((c = reader.read()) != -1){
              System.out.print((char)c);
          } 
          reader.close();
      } 
      ...
    

文件数据IO操作

PrintWriter

  • 创建PrintWriter对象

    PrintWriter是具有自动行刷新的缓冲该字符输出流。其提供了比较丰富的构造方法:

      PrintWriter(File file)
      PrintWriter(String fileName)
      PrintWriter(OutputStream out)
      PrintWriter(OutputStream out,boolean autoFlush)
      PrintWriter(Writer writer)
      PrintWriter(Writer writer,boolean autoFlush)
    

    其中参数为OutputStream与Writer的构造方法提供了一个可以传入boolean值参数,该参数用于表示PrintWriter是否具有自动行刷新。

  • PrintWriter的重载print和println方法

    使用PrintWriter写出字符串时我们通常不使用Writer提供的write()相关方法,而是使用print和println等方法,PrintWriter提供了若干重载的print与println方法,其中println方法是在写出数据后自动追加一个系统支持的换行符。

    重载方法有:

      void print(int i)//打印整数 
      void print(char c)//打印字符 
      void print(boolean b)//打印boolean值 
      void print(char[] c)//打印字符数组 
      void print(double d)//打印double值 
      void print(float f)//打印float值 
      void print(long l)//打印long值 
      void print(String str)//打印字符串 
    

    注:这些方法还有类似的println方法,参数与上面相同。

  • 使用PW输出字符数据

      FileOutputStream fos 
          = new FileOutputStream("demo.txt");
      OutputStreamWriter osw 
          = new OutputStreamWriter(fos,"UTF-8");
      //创建带有自动行刷新的PW
      PrintWriter pw = new PrintWriter(osw,true);
      pw.println("大家好!");
      pw.close();
    

BufferedReader

  • 构建BufferedReader对象

    BufferedReader是缓冲字符输入流,其内部提供了缓冲区,可以提高读取效率。

    BufferedReader的常用构造方法:

      BufferedReader(Reader reader)
    

    例如:

      FileInputStream fis 
          = new FileInputStream("demo.txt");
      InputStreamReader isr 
          = new InputStreamReader(fis);
      BufferedReader br 
          = new BufferedReader(isr);
      …. 
    

    注:因为BufferedReader在构造实例时需要传入一个字符流,所以当我们想基于一个字节流进行读取时,要先将字节流转换为字符流后方可创建缓冲字符输入流BufferedReader。

  • 使用BR读取字符串

    BufferedReader提供了一个可以便于读取一行字符串:

      String readLine()
    

    该方法连续读取一行字符串,直到读取到换行符为止,返回的字符串中不包含该换行符。若EOF则该方法返回null。

    例如:

      FileInputStream fis 
          = new FileInputStream("demo.txt");
      InputStreamReader isr 
          = new InputStreamReader(fis);
      BufferedReader br 
          = new BufferedReader(isr);
      String str = null;
      while((str = br.readLine()) != null){
          System.out.println(str);
      } 
      br.close();
    

异常处理

异常处理概述

  • 使用返回值状态标识异常

    在JAVA语言出现以前,传统的异常处理方式多采用返回值来标识程序出现的异常情况,这种方式虽然为程序员所熟悉,但却有多个坏处。

    首先,一个API可以返回任意的返回值,而这些返回值本身并不能解释该返回值是否代表一个异常情况发生了和该异常的具体情况,需要调用API的程序自己判断并解释返回值的含义。

    其次,并没有一种机制来保证异常情况一定会得到处理,调用程序可以简单的忽略该返回值,需要调用API的程序员记住去检测返回值并处理异常情况。这种方式还让程序代码变得冗长,尤其是当进行IO操作等容易出现异常情况的处理时,代码的很大部分用于处理异常情况的switch分支,程序代码的可读性变得很差。

  • 异常处理机制

    当程序中抛出一个异常后,程序从程序中导致异常的代码处跳出,java虚拟机检测寻找和try关键字匹配的处理该异常的catch块,如果找到,将控制权交到catch块中的代码,然后继续往下执行程序,try块中发生异常的代码不会被重新执行。如果没有找到处理该异常的catch块,在所有的finally块代码被执行和当前线程的所属的ThreadGroup的uncaughtException方法被调用后,遇到异常的当前线程被中止。

异常的捕获和处理

  • Throwable,Error和Exception

    Java异常结构中定义有Throwable类,Exceotion和Error是其派生的两个子类。其中Exception表示由于网络故障、文件损坏、设备错误、用户输入非法等情况导致的异常,这类异常是可以通过Java异常捕获机制处理的。而Error表示Java运行时环境出现的错误,例如:JVM内存溢出等。

  • try-catch

    try {...} 语句指定了一段代码,该段代码就是一次捕获并处理例外的范围。在执行过程中,该段代码可能会产生并抛出一种或几种类型的异常对象,它后面的catch语句分别对这些异常做相应的处理。

    如果没有列外产生,所有的catch代码段都被略过不执行

    在catch语句块中是对异常进行处理的代码。catch中声明的异常对( catch(SomeException e) )封装了异常事件发生的信息,在catch语句块中可以使用这个对象的一些方法获取这些信息

    常见格式:

          ...
          try{
              //可能出现异常的代码片段
          }catch(Exception e){
              //处理该异常的代码片段
          }
          ...
    
  • 多个catch

    每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常 。catch捕获的异常类型由上至下的捕获异常类型的顺序应是子类到父类的

    例如

      try{
          … 
      }catch(NullPointerException e){    //子类异常应在上面捕获
          …
      }catch(RuntimeException e){        //父类异常在下面捕获
            …    
      }catch(Exception e){            //应养成最终捕获Exception的习惯
            …
      } 
    

    通常在书写代码的时候,我们应当在最后一个catch中捕获Exception,这样可以保证代码不会因为出现一个未在catch中声明的异常而导致捕获失败使得程序终止。

  • finally的作用

    finally语句为异常处理提供一个统一的出口,使得在控制流程转到程序其它部分以前,能够对程序的状态作统一管理。

    无论try所指定的程序块中是否抛出例外,finally所指定的代码都要被执行,通常在finally语句中可以进行资源的消除工作,如关闭打开的文件、删除临时文件等。

    finally语句块只能定义在try语句块之后,或者最后一个catch语句块之后,且只能定义一次。

  • throw关键字

    当程序发生错误而无法处理的时候,会抛出对应的异常对象,除此之外,在某些时刻,您可能会想要自行抛出异常,例如在异常处理结束后,再将异常抛出,让下一层异常处理区块来捕捉,若想要自行抛出异常,您可以使用“throw”关键词,并生成指定的异常对象。

    例如:

      throw new ArithmeticException();
    
  • throws关键字

    程序中会声明许多方法(Method),这些方法中可能会因某些错误而引发异常,但您不希望直接在这个方法中处理这些异常,而希望调用这个它的方法来统一处理,这时候您可以使用“throws”关键词来声明这个方法将会抛出异常

    例如:

      public static void stringToDate(String str) throws  ParseException{               ……
      }
    
  • 重写方法时的throws

    当使用继承时,在父类的某个方法上声明了throws抛出某些异常,而在子类中重写该方法时,我们可以做以下的操作:

      1. 不处理异常(重写方法时不声明throws)
      2. 可仅在throws中声明父类中声明的部分异常
      3. 可在throws中声明父类方法中抛出的异常的子类异常
    

    但是不能做以下操作:

      1. 重写方法时在throws中声明抛出额外的异常
      2. 重写方法时在throws中声明父类方法中声明的抛出异常的父类异常
    

    示例代码:

      public class Father {
      	public void dosome()
      			throws IOException,AWTException{
    
      	}
      }	
    
      public class Son extends Father{
      	//可以不再声明抛出任何异常
      //	public void dosome(){
      //		
      //	}
    
      	//可以仅抛出父类抛出的部分异常
      //	public void dosome()throws IOException{
      //		
      //	}
    
      	/*
      	 * 可以抛出父类抛出异常的子类异常
      	 */
      //	public void dosome()
      //			throws FileNotFoundException{
      //		
      //	}
    
      	/*
      	 * 不可以抛出与父类抛出异常没有继承关系的其他异常
      	 */
      //	public void dosome()throws SQLException{
      //		
      //	}
    
      	/*
      	 * 不可以抛出父类抛出异常的父类异常 
      	 */
      //	public void dosome()throws Exception{
      //		
      //	}
      }	
    

Java异常API

  • RuntimeException

    Java异常可以分为可检测异常,非检测异常

    1. 可检测异常:可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则,不捕捉这个异常,编译器就通不过,不允许编译
    2. 非检测异常:非检测异常不遵循处理或者声明规则。在产生此类异常时,不一定非要采取任何适当操作,编译器不会检查是否已经解决了这样一个异常

    RuntimeException 类属于非检测异常,因为普通JVM操作引起的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在java应用程序中会频繁出现。因此它们不受编译器检查与处理或声明规则的限制 。

  • 常见RuntimeException

      IllegalArgumentException
      抛出的异常表明向方法传递了一个不合法或不正确的参数
      NullPointerException
      当应用程序试图在需要对象的地方使用 null 时,抛出该异常
      ArrayIndexOutOfBoundsException
      当使用的数组下标超出数组允许范围时,抛出该异常
      ClassCastException
      当试图将对象强制转换为不是实例的子类时,抛出该异常
      NumberFormatException
      当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
    

Exception常用API

  • printStackTrace

    Throwable中定义了一个方法可以输出错误信息,用来跟踪异常事件发生时执行堆栈的内容。该方法定义为:

      	void printStackTrace()
    

    例如:

          try{
              … 
          }catch(Exception e){
              e.printStackTrace();//输出执行堆栈信息 
          }
    
  • getMessage

    Throwable中定义了一个方法可以得到有关异常事件的信息。该方法定义为:

          String getMessage()
    

    例如:

          try{
              … 
          }catch(Exception e){
              System.out.println(e.getMessage());
          }
    
  • getCause

    很多时候,当一个异常由另一个异常导致异常而被抛出的时候,Java库和开放源代码会将一种异常包装成另一种异常。这时,日志记录和打印根异常就变得非常重要。Java异常类提供了 getCause()方法来检索导致异常的原因,这些可以对异常根层次的原因提供更多的信息。该Java实践对代码的调试或故障排除有很大的帮助。另外,如果要把一个异常包装成另一种异常,构造一个新异常就要传递源异常。

      Throwable getCause()
    

    获取该异常出现的原因。

自定义Exception

  • 自定义异常的意义

    Java异常机制可以保证程序更安全和更健壮。虽然Java类库已经提供很多可以直接处理异常的类,但是有时候为了更加精准地捕获和处理异常以呈现更好的用户体验,需要开发者自定义异常。

  • 继承Exception自定义异常

    创建自定义异常类,语法格式:

      public class [自定义异常类名] extends Exception{
          … 
      }
    
  • 如何编写构造方法

    当定义好自定义异常后,我们可以通过Eclipse来自动生成相应的构造方法。

    具体步骤如下:

    1. 声明一个类并继承自Exception
    2. 右键点击Source
    3. 选择Generate Constructors from Superclass
    4. 选中父类中所有构造方法后确认生成

    例如:

      public class MyException extends Exception{
          public MyException() {
              super();
          // TODO Auto-generated constructor stub
          }
          public MyException(String message, Throwable cause) {
              super(message, cause);
              // TODO Auto-generated constructor stub
          }
          public MyException(String message) {
              super(message);
              // TODO Auto-generated constructor stub
          }
          public MyException(Throwable cause) {
              super(cause);
              // TODO Auto-generated constructor stub
          }
    
      }
    

多线程基础

进程和线程

  • 什么是进程

    所谓进程(process)就是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。进程中所包含的一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。操作系统中有若干个线程在"同时"运行。通常,操作系统上运行的每一个应用程序都运行在一个进程中,例如:QQ,IE等等。

    注:进程并不是真正意义上的同时运行,而是并发运行。后面我们会具体说明。

  • 什么是线程

    一个线程是进程的一个顺序执行流。同类的多个线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行时的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程中可以包含多个线程。

    注:切换——线程并发时的一种现象,后面讲解并发时会具体说明。

  • 进程与线程的区别

    一个进程至少有一个线程。线程的划分尺度小于进程,使得多线程程序的并发性高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

    线程在执行过程中与进程的区别在于每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

    从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配。

  • 线程使用的场合

    线程通常用于在一个程序中需要同时完成多个任务的情况。我们可以将每个任务定义为一个线程,使他们得以一同工作。

    例如我们在玩某个游戏时,这个游戏由操作系统运行,所以其运行在一个独立的进程中,而在游戏中我们会听到某些背景音乐,某个角色在移动,出现某些绚丽的动画效果等,这些在游戏中都是同时发生的,但实际上,播放音乐是在一个线程中独立完成的,移动某个角色,播放某些特效也都是在独立的线程中完成的。这些事情我们无法在单一线程中完成。

    也可以用于在单一线程中可以完成,但是使用多线程可以更快的情况。比如下载文件。

    比如迅雷,我们尝尝会开到它会打开很多个节点来同时下载一个文件。

  • 并发原理

    通过上面几节知识我们知道进程与线程都是并发运行的,那么什么是并发呢?

    多个线程或进程”同时”运行只是我们感官上的一种表现。事实上进程和线程是并发运行的,OS的线程调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待。而CPU则在这些进程或线程上来回切换运行。微观上所有进程和线程是走走停停的,宏观上都在运行,这种都运行的现象叫并发,但是不是绝对意义上的“同时发生。

    注1:之所以这样做是因为CPU只有一个,同一时间只能做一件事情。但随着计算机的发展,出现了多核心CPU,例如两核心的CPU可以实现真正意义上的2线程同时运行,但因为CPU的时间片段分配给那个进程或线程是由线程调度决定,所以不一定两个线程是属于同一个进程的,无论如何我们只需要知道线程或进程是并发运行即可。

    注2:OS—Operating System我们称为:操作系统

    注3:线程调度机制是OS提供的一个用于并发处理的程序。java虚拟机自身也提供了线程调度机制,用于减轻OS切换线程带来的更多负担。

  • 线程状态

    对于程序而言,我们实际上关心的是线程而非进程。通过上面学习的只是,我们了解了什么是线程以及并发的相关知识。那么我们来看看线程在其生命周期中的各个状态:

    线程的生命周期

      New:当我们创建一个线程时,该线程并没有纳入线程调度,其处于一个new状态。
      Runnable:当调用线程的start方法后,该线程纳入线程调度的控制,其处于一个可运行状态,等待分配时间片段以并发运行。
      Running:当该线程被分配到了时间片段后其被CPU运行,这是该线程处于running状态。
      Blocked:当线程在运行过程中可能会出现阻塞现象,比如等待用户输入信息等。但阻塞状态不是百分百出现的,具体要看代码中是否有相关需求。
      Dead:当线程的任务全部运行完毕,或在运行过程中抛出了一个未捕获的异常,那么线程结束,等待GC回收
    

创建线程

  • 使用Thread创建线并启动线程

    java.lang.Thread类是线程类,其每一个实例表示一个可以并发运行的线程。我们可以通过继承该类并重写run方法来定义一个具体的线程。其中重写run方法的目的是定义该线程要执行的逻辑。启动线程时调用线程的start()方法而非直接调用run()方法。start()方法会将当前线程纳入线程调度,使当前线程可以开始并发运行。当线程获取时间片段后会自动开始执行run方法中的逻辑。

    例如:

      public class TestThread extends Thread{
          @Override
          public void run() {
              for(int i=0;i<100;i++){
                  System.out.println("我是线程");
              }
          }
      }
    

    创建和启动线程:

      …
      Thread thread = new TestThread();//实例化线程 
      thread.start();//启动线程 
      …
    

    当调用完start()方法后,run方法会很快执行起来。

  • 使用Runnable创建并启动线程

    实现Runnable接口并重写run方法来定义线程体,然后在创建线程的时候将Runnable的实例传入并启动线程。

    这样做的好处在于可以将线程与线程要执行的任务分离开减少耦合,同时java是单继承的,定义一个类实现Runnable接口这样的做法可以更好的去实现其他父类或接口。因为接口是多继承关系。

    例如:

      public class TestRunnable implements Runnable{
          @Override
          public void run() {
              for(int i=0;i<100;i++){
                  System.out.println("我是线程");
              }
          }
      }
    

    启动线程的方法:

      …
      Runnable runnable = new TestRunnable();
      Thread thread = new Thread(runnable);//实例化线程并传入线程体 
      thread.start();//启动线程 
      …
    
  • 使用内部类创建线程

    通常我们可以通过匿名内部类的方式创建线程,使用该方式可以简化编写代码的复杂度,当一个线程仅需要一个实例时我们通常使用这种方式来创建。

    例如:

    继承Thread方式:

      Thread thread  = new Thread(){    //匿名类方式创建线程 
          public void run(){
              //线程体    
          }
      };
      thread.start();//启动线程    
    

    Runnable方式:

      Runnable runnable  = new Runnable(){    //匿名类方式创建线程 
          public void run(){    
          }
      };
      Thread thread = new Thread(runnable);
      thread.start();//启动线程 
    

线程操作API

  • Thread.currentThread方法

    Thread的静态方法currentThread方法可以用于获取运行当前代码片段的线程。

      Thread current = Thread.currentThread();    
    
  • 获取线程信息

    Thread提供了 获取线程信息的相关方法:

      long getId():返回该线程的标识符
      String getName():返回该线程的名称
      int getPriority():返回线程的优先级
      Thread.state getState():获取线程的状态
      boolean isAlive():测试线程是否处于活动状态
      boolean isDaemon():测试线程是否为守护线程
      boolean isInterrupted():测试线程是否已经中断
    

    例如:

      public class TestThread {
          public static void main(String[] args) {
              Thread current = Thread.currentThread();
              long id = current.getId();
              String name = current.getName();
              int priority = current.getPriority();
              Thread.State state = current.getState();
              boolean isAlive = current.isAlive();
              boolean isDaemon = current.isDaemon();
              boolean isInterrupt = current.isInterrupted();
              System.out.println("id:"+id);
              System.out.println("name:"+name);
              System.out.println("priority:"+priority);
              System.out.println("state:"+state);
              System.out.println("isAlive:"+isAlive);
              System.out.println("isDaemon:"+isDaemon);
              System.out.println("isInterrupt:"+isInterrupt);
          }
      } 
    
  • 线程优先级

    线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们可以通过提高线程的优先级来最大程度的改善线程获取时间片的几率。

    线程的优先级被划分为10级,值分别为1-10,其中1最低,10最高。线程提供了3个常量来表示最低,最高,以及默认优先级:

      Thread.MIN_PRIORITY,
      Thread.MAX_PRIORITY,
      Thread.NORM_PRIORITY
    

    设置优先级的方法为:

      void setPriority(int priority)
    
  • 守护线程

    守护线程与普通线程在表现上没有什么区别,我们只需要通过Thread提供的方法来设定即可:

      void setDaemon(boolean )
    

    当参数为true时该线程为守护线程。

    守护线程的特点是,当进程中只剩下守护线程时,所有守护线程强制终止。

    GC就是运行在一个守护线程上的。

    需要注意的是,设置线程为后台线程要在该线程启动前设置。

      Thread daemonThread = new Thread();
      daemonThread.setDaemon(true);
      daemonThread.start();
    

    示例代码:

      /*
       * rose:扮演者为前台线程
       */
      Thread rose = new Thread(){
      	public void run(){
      		for(int i=0;i<10;i++){
      			System.out.println(
      				"rose:let me go!"
      			);
      			try {
      				Thread.sleep(1000);
      			} catch (InterruptedException e) {
      			}
      		}
      		System.out.println(
      			"rose:啊啊啊啊AAAAAaaaaa.....");
      		System.out.println("效果:噗通!");
      	}
      };
      /*
       * jack:扮演者后台线程
       */
      Thread jack = new Thread(){
      	public void run(){
      		while(true){
      			System.out.println(
      				"jack:you jump!i jump!"
      			);
      			try {
      				Thread.sleep(1000);
      			} catch (InterruptedException e) {
      			}
      		}
      	}
      };
      //设置后台线程要在start方法调用前进行
      jack.setDaemon(true);
    
    
      rose.start();
      jack.start();
    
  • sleep方法

    Thread的静态方法sleep用于使当前线程进入阻塞状态:

      static void sleep(long ms)
    

    该方法会使当前线程进入阻塞状态指定毫秒,当指定毫秒阻塞后,当前线程会重新进入Runnable状态,等待分配时间片。

    该方法声明抛出一个InterruptException。所以在使用该方法时需要捕获这个异常。

    例如:电子钟程序

      public static void main(String[] args) {
          SimpleDateFormat sdf
              = new SimpleDateFormat("hh:mm:ss");
          while(true){
              System.out.println(sdf.format(new Date()));
              try {
                  Thread.sleep(1000);//每输出一次时间后阻塞1秒钟
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
    

    注:改程序可能会出现"跳秒"现象,因为阻塞一秒后线程并非是立刻回到running状态,而是出于runnable状态,等待获取时间片。那么这段等待时间就是"误差"。所以以上程序并非严格意义上的每隔一秒钟执行一次输出。

  • yield方法

    Thread的静态方法yield:

      static void yield()
    

    该方法用于使当前线程主动让出当次CPU时间片回到Runnable状态,等待分配时间片。

  • join方法

    Thread的方法join:

          void join()
    

    该方法用于等待当前线程结束。此方法是一个阻塞方法。

    该方法声明抛出InterruptException。

    例如:

      public static void main(String[] args) {
          final Thread t1 = new Thread(){
              public void run(){
                  //一些耗时的操作
              }
          };
    
          Thread t2 = new Thread(){
              public void run(){
                  try {
                      t1.join();//这里t2线程会开始阻塞,直到t1线程的run方法执行完毕
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  //以下是当前线程的任务代码,只有t1线程运行完毕才会运行。
              }
          };
      }
    

    示例代码:

      //用来标示图片是否下载完毕的一个状态
      public static boolean isFinish;
      public static void main(String[] args) {
      	final Thread download = new Thread(){
      		public void run(){
      			System.out.println("down:开始下载图片...");
      			for(int i=1;i<=100;i++){
      				System.out.println("down:"+i+"%");
      				try {
      					Thread.sleep(50);
      				} catch (InterruptedException e) {
      					e.printStackTrace();
      				}
      			}
      			System.out.println("down:图片下载完毕!");
      			isFinish = true;
      		}
      	};
    
      	Thread show = new Thread(){
      		public void run(){
      			System.out.println("show:开始显示图片!"); 	
      			/*
      			 * 在这里等待download将图片下载完毕
      			 */
      			try{
      				download.join();
      			}catch(InterruptedException e){
      				e.printStackTrace();
      			}
      			if(!isFinish){
      				throw new RuntimeException("图片没有下载完毕!");
      			}
      			System.out.println("show:显示图片完毕!");
      		}
      	};
    
    
      	download.start();
      	show.start();
      }
    

线程同步

  • synchronized关键字

    多个线程并发读写同一个临界资源时候会发生"线程并发安全问题“

      常见的临界资源:
      多线程共享实例变量
      多线程共享静态公共变量
    

    若想解决线程安全问题,需要将异步的操作变为同步操作。 何为同步?那么我们来对比看一下什么是同步什么异步。

      所谓异步操作是指多线程并发的操作,相当于各干各的。
      所谓同步操作是指有先后顺序的操作,相当于你干完我再干。
    

    而java中有一个关键字名为:synchronized,该关键字是同步锁,用于将某段代码变为同步操作,从而解决线程并发安全问题。

  • 锁机制

    Java提供了一种内置的锁机制来支持原子性:

    同步代码块(synchronized 关键字 ),同步代码块包含两部分:一个作为锁的对象的引用,一个作为由这个锁保护的代码块。

      synchronized (同步监视器—锁对象引用){ 
          //代码块
      } 
    

    若方法所有代码都需要同步也可以给方法直接加锁。

    每个Java对象都可以用做一个实现同步的锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时释放锁,而且无论是通过正常路径退出锁还是通过抛异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

  • 选择合适的锁对象

    使用synchroinzed需要对一个锁对象上锁以保证线程同步。

    那么这个锁对象应当注意:多个需要同步的线程在访问该同步块时,看到的应该是同一个锁对象引用。否则达不到同步效果。 通常我们会使用this来作为锁对象。

  • 选择合适的锁范围

    在使用同步块时,应当尽量在允许的情况下减少同步范围,以提高并发的执行效率。

  • 静态方法锁

    当我们对一个静态方法加锁,如:

      public synchronized static void xxx(){
          ….
      }
    

    那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class。

    静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的。原因在于,静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。

  • wait和notify

    多线程之间需要协调工作。

    例如,浏览器的一个显示图片的 displayThread想要执行显示图片的任务,必须等待下载线程downloadThread将该图片下载完毕。如果图片还没有下载完,displayThread可以暂停,当downloadThread完成了任务后,再通知displayThread“图片准备完毕,可以显示了”,这时,displayThread继续执行。

    以上逻辑简单的说就是:如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒。在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。

    示例代码:

      public static Object obj = new Object();
    
      //用来标示图片是否下载完毕的一个状态
      public static boolean isFinish;
    
      public static void main(String[] args) {
    
      	final Thread download = new Thread(){
    
      		public void run(){
      			System.out.println("down:开始下载图片...");
      			for(int i=1;i<=100;i++){
      				System.out.println("down:"+i+"%");
      				try {
      					Thread.sleep(50);
      				} catch (InterruptedException e) {
      				}
      			}
      			System.out.println("down:图片下载完毕!");
      			isFinish = true;
      			/*
      			 * 当图片下载完毕,就可以通知显示线程开始
      			 * 显示图片
      			 */
      			synchronized (obj) {
      				obj.notify();
      			}			
    
      			//继续下载附件
      			System.out.println("down:开始下载附件...");
      			for(int i=1;i<=100;i++){
      				System.out.println("down:"+i+"%");
      				try {
      					Thread.sleep(50);
      				} catch (InterruptedException e) {
      				}
      			}
      			System.out.println("down:附件下载完毕!");
      		}
      	};
    
      	Thread show = new Thread(){
      		public void run(){
      			System.out.println("show:开始显示图片!");
      			/*
      			 * 在这里等待download将图片下载完毕
      			 */
      			try{
      				// download.join();
      				synchronized (obj) {
      					obj.wait();
      				}
      			}catch(InterruptedException e){
    
      			}
      			if(!isFinish){
      				throw new RuntimeException("图片没有下载完毕!");
      			}
      			System.out.println("show:显示图片完毕!");
      		}
      	};
    
    
      	download.start();
      	show.start();
      }
    
  • 线程安全API与非线程安全API

    之前学习的API中就有设计为线程安全与非线程安全的类:

      StringBuffer 是同步的 synchronized append();
      StringBuilder 不是同步的 append();
    

    相对而言StringBuffer在处理上稍逊于StringBuilder,但是其是线程安全的。当不存在并发时首选应当使用StringBuilder。

    同样的:

    Vector 和 Hashtable 是线程安全的而ArrayList 和 HashMap则不是线程安全的。

    对于集合而言,Collections提供了几个静态方法,可以将集合或Map转换为线程安全的:

    例如:

    Collections.synchronizedList() :获取线程安全的List集合 Collections.synchronizedMap():获取线程安全的Map ... List<String> list = new ArrayList<String>(); list.add("A"); list.add("B"); list.add("C"); list = Collections.synchronizedList(list);//将ArrayList转换为线程安全的集合 System.out.println(list);//[A,B,C] 可以看出,原集合中的元素也得以保留 ...

  • 使用ExecutorService实现线程池

    当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。为此我们应使用线程池来解决这个问题。

    ExecutorService是java提供的用于管理线程池的类。

      线程池有两个主要作用:
      控制线程数量
      重用线程
    

    线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。

    在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务

      线程池有以下几种实现策略:
      	Executors.newCachedThreadPool()
      	创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
      	Executors.newFixedThreadPool(int nThreads)
      	创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
      	Executors.newScheduledThreadPool(int corePoolSize)
      	创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
      	Executors.newSingleThreadExecutor()
      	创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
    

    可以根据实际需求来使用某种线程池。例如,创建一个有固定线程数量的线程池:

      ...
      ExecutorService threadPool 
          = Executors.newFixedThreadPool(30);//创建具有30个线程的线程池
      Runnable r1 = new Runable(){
          public void run(){
              //线程体
          }
      };
      threadPool.execute(r1);//将任务交给线程池,其会分配空闲线程来运行这个任务。
      ...
    
  • 使用BlockingQueue

    BlockingQueue是双缓冲队列。

    在多线程并发时,若需要使用队列,我们可以使用Queue,但是要解决一个问题就是同步,但同步操作会降低并发对Queue操作的效率。

    BlockingQueue内部使用两条队列,可允许两个线程同时向队列一个做存储,一个做取出操作。在保证并发安全的同时提高了队列的存取效率。

    双缓冲队列有一下几种实现:

      ArrayBlockingDeque:规定大小的BlockingDeque,其构造函数必须带一个int参数来指明其大小.其所含	的对象是以FIFO(先入先出)顺序排序的。
      LinkedBlockingDeque:大小不定的BlockingDeque,若其构造函数带一个规定大小的参数,生成的
      	BlockingDeque有大小限制,若不带大小参数,所生成的BlockingDeque的大小Integer.MAX_VALUE
      	来决定.其所含的对象是以FIFO(先入先出)顺序排序的。
      PriorityBlockingDeque:类似于LinkedBlockDeque,但其所含对象的排序不是FIFO,而是依据对象的
      	自然排序顺序或者是构造函数的Comparator决定的顺序。
      SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。
    

    例如:

      public static void main(String[] args) {
          BlockingQueue<String> queue
              = new LinkedBlockingDeque<String>();
    
          try {
              //queue.offer("A");//立即向队列末尾追加元素
    
              /*
               * 向队列末尾追加元素,指定可延迟5秒。
               * 若5秒钟内成功将元素加入队列返回true
               * 若超时后元素仍然没有加入队列则返回flase
               */
              queue.offer("A",5,TimeUnit.SECONDS);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println(queue.poll());
      }
    

转载于:https://my.oschina.net/u/2619068/blog/809046

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值