认识 Java
凡事都需要从最开始的了解开始,我们来认识一下 Java 这门语言。
Java 是于 1995 年由 Sun 公司推出的一种极富创造力的 面对对象 的程序设计语言,最初的名字是 OAK,再 95 年被重命名为 Java。Java 是一种 跨平台、语言简洁、可靠性强以及拥有较高安全性 的一门编程语言。Java 按照应用范围可以分成 3 个版本,分别时 Java SE、Java EE、Java ME。Java SE 只要是用于 桌面应用的开发,也时Java 的基础,包含了Java语言基础、JDBC、I/O、网络通信、多线程等技术。Java EE 是 Java企业版,主要用于开发企业级分布式网络程序,比如 电子商务网站、ERP(企业资源规划)系统。Java ME 只要应用于嵌入式开发,比如手机、平板等移动通信设备。
更多可以 百度搜索Java
熟悉开发工具
开发工具的学习没有其它,多使用自然就熟练了。
常用的开发工具有,Eclipse、MyEclipse、idea。
入门建议使用 Eclipse ,比较简单容易上手,熟悉了之后使用 idea ,集成功能丰富。
Java 语言基础(数据类型与常识)
Java基本数据类型主要分为:整数类型(byte、shot、int、long)、浮点类型(float、double)、字符类型(char) 和 布尔类型(boolean) 四大类型,也被称之为 8 种基本数据类型。
整数类型:整数类型用来储存整数数值,储存的数值不包含小数部分。可以是正数,也可以是负数,整型的数据可以用八进制、十进制以及十六进制来表示。但是需要注意:十进制除去 0 以外的其它十进制数不能以 0 开头,比如 01、09等。八进制的数字必须以 0 开头,比如 0123(十进制为 83)。十六进制的话必须以 0X 或者 0x 开头,比如 0x25(十进制为 37)、0xb01e(十进制为 45086)。对于整数类型中 4种基本数据类型的选择,可以参考它们的取值范围。
| 数据类型 | 内存空间(8位等于1字节) | 取值范围 |
|---|---|---|
| byte | 8位 | -128~127 |
| short | 16位 | -32768~32767 |
| int | 32位 | -2147483648~2147483647 |
| long | 64位 | -9223372036854775808~9223372036854775807 |
赋值给 long 类型时,需要在数值后面加入 l 或 L,比如 long num = 123456789L 。
浮点类型:浮点类型表示有小数部分的数字,简单言之就是带小数点的数字。浮点类型中分为 单精度浮点类型(float)和 双精度浮点类型(double),他们拥有不同的取值范围,如下图:
| 数据类型 | 内存空间(8位等于1字节) | 取值范围 |
|---|---|---|
| float | 32位 | 1.4E-45~2.4028235E38 |
| double | 64位 | 4.9E-324~1.7976931348623157E308 |
在默认的情况下,小数会被看作 double 型,如果要使用 float 型的小数,需要在小数后面加入 F 或者 f,如 1.2332f、-3.1415F等。对于 double 类型的话,加不加 D 或 d 都不会出错。
科普一下,在科学与工程领域,“e”代表自然对数的基数,约等于2.718,但是在Java、c 以及 c++ 中表示的是 10。比如 1.24E12 表示为 1.24 乘以 10 的 12次方。
字符类型:字符类型(char)用于储存单个字符,占用 16位 的内存空间。在定义字符型变量时要用单引号表示,如 char c = ‘a’; 因为字符 a 在 Unicode 表中 排序位置为 97,所以也可以写为 char c = 97; 还有一种特殊的字符变量,我们称为 转义字符,它具有特定的含义,不同于字符原有的意义,所以称为“转义”。见下表:
| 转义字符 | 含义 |
|---|---|
| ddd | 1~3位八进制数据所表示的字符,如 456 |
| dxxxx | 4位十六进制所表示的字符,如 052 |
| ’ | 单引号字符 |
| \ | 反斜杠字符 |
| t | 垂直制表符,将光标移到下一个制表符的位置 |
| r | 回车 |
| n | 换行 |
| b | 退格 |
| f | 换页 |
需要注意的是,将转义字符赋值给字符变量时,一样需要使用单引号。
布尔类型:布尔类型也被称为逻辑类型,它只有 true 和 false 两个值。分别代表布尔逻辑中的 “真” 和 “假”。布尔类型不能与其它类型进行转换,通常只在流程控制中作为判断条件。
其它基础的定义了解一下:
变量:可以改变值的量称为变量;
常量:值不能改变的量称为常量;
标识符:可以理解为将一个篮子起了个名字,东西可以放在篮子里,要用篮子里面的东西的时候只需要通过名字就可以取到。要注意的是,Java中对标识符中的字母是严格区分大小写的。
关键字:关键字是固定的,是被赋予特定意义的单词,不能将这些单词作为标识符使用。
变量的有效范围:看离变量最近的 { } ,在最近的 { } 中都是有效范围。
运算符:赋值运算符【=】、算数运算符 ( 加【+】、减【-】、乘【*】、除【/】、取余数【%】 ) 、比较运算符 ( 大于【>】、小于【<】、等于【==】、大于等于【>=】、小于等于【<=】、不等于【!=】 ) 、逻辑运算符 ( 或【||】、与【&&】、非【!】 ) 等。
运算符优先级:增量和减量运算 =》 算术运算 =》比较运算 =》逻辑运算 =》赋值运算。如果两边优先级相同,先处理左边的。
类型转换:低类型向高类型转换是自动进行的,也称为隐式转换,比如:int x = 10;float y = x;反之需要进行显式转换,也就是需要强转。比如:int a = (int)45.23;
Java 语言基础(逻辑语句)
复合语句:以 { } 包裹的区块为单位的语句称为复合语句。复合语句中可以嵌套复合语句。
条件语句:条件语句可以根据不同条件执行不同的语句。Java 中有if语句、if...else语句、if...else if多分支语句以及switch 多分支语句。需要注意的是,在if 类型的语句中只执行条件为 true 的语句,在 switch 多分支语句中 case 的常量值必须互不相同而且不能为实数(如:case 1.1: )。
循环语句:循环语句中包含了 while 、do while 、for 以及 foreach 语句。具体了解一下 foreach 语句的语法,for(元素变量 x : 遍历对象 obj){ 引用了 x 的 Java 语句; }。
Java 语言基础(字符串)
字符串是 Java 程序中经常使用的对象,也是非常重要的一点。在Java 中字符串作为 String 类的实例来处理。
String类:之前基本类型中的 char 类型只能表示单个字符,而 String 对象类 解决了这个问题。在 Java 中只要是 “”包围的都是字符串。声明的字符串变量必须要初始化才能使用。最需要注意的是,在 Java 中 String 字符串创建了之后是只读的,也就是不会发生改变的,所以如果需要大量的修改字符串内容可以使用 StringBuilder 或者 StringBuffer。虽然底层编译原理中,直接使用String来处理字符串,编译器也会自动创建一个StringBuilder对象用来构建最后的String并保存,但是在多次使用String对象的时候(比如循环中为字符串添加字符的操作),底层的编译一样会多次的构建 StringBuilder 对象,所以对于多次修改字符串的操作使用 StringBuilder对象是更节省内存空间的!
正则表达式:正则表达式是含有一些具有特殊意义字符的字符串,这些特殊字符称为正则表达式的元字符。
| 元字符 | 意义 |
|---|---|
| . | 代表任意一个字符 |
| d | 代表 0~9 的任意一个数字 |
| D | 代表任意一个非数字字符 |
| s | 代表空白字符,如 ‘t’ ‘n’ |
| S | 代表非空白字符 |
| w | 代表可用作标识符的字符,但不包括‘$’ |
| W | 代表不可用于标识符的字符 |
| [^456] | 代表4、5、6之外的任意字符 |
| [a-r] | 代表 a~r中的任意一个字母 |
| [a-e[g-z]] | 代表 a ~ e,或g ~ z 中的任何字母(并集) |
| [a-o$$[def]] | 代表字母 d、e、f(交集) |
| [a-d$$[^bc]] | 代表字符 a、d(差集) |
| A? | A出现0次或1次 |
| A* | A出现0次或多次 |
| A+ | A出现一次或多次 |
| A{n} | A正好出现n次 |
| A{n,} | A至少出现n次 |
| A{n,m} | A出现n ~ m次 |
还有很多元字符没有列出,具体的可以参考 JDK 文档 java.util.regex 包中的 Pattern 类。
Java 语言基础(数组)
数组是最为常见的一种数据结构,是相同类型、用一个标识符封装到一起的基本数据类型数据序列或对象序列。本质上,数组是一个简单的线性序列,因此访问的速度很快。使用白话来讲的话,就是我们将足球、篮球、乒乓球放入一个盒子中,并将它们统称为球类,球类也就是他们的标识符。
对于创建的整型一维二维数组,数组中的每个元素的初始值都是 0 。
遍历数组:遍历数组就是获得数组中的每个元素,通常使用 for 循环来实现的。遍历一维数组比较简单,对于遍历二维数组的时候,我们需要使用双层 for 循环。
Java 语言基础(对象与类)
在 Java 中最常提到的就是类和对象,实质上可以将类看为对象的载体,类中定义了对象所具有的功能。白话来说,每个类中都有“属性”和“功能”。属性就是类中定义的姓名、年龄等名词,如 public String name;//姓名。功能,也被称为行为或方法,用动词表示,比如 做自我介绍 public void selfIntroduction(){ //自我介绍的实现语句 }。我们需要理解对象的概念,从而深度理解面向对象的思想。经典的一句话:万物皆对象。
封装:属于面向对象编程的核心思想。将对象的属性与行为封装起来,载体就是类,类通常对用户隐藏其中实现的细节,这个就是封装的思想。白话解释的话,如同我们使用计算机来打开文件,我们不需要知道计算机内部是怎么样实现的,我们只需要通过鼠标键盘来打开文件就好了。就算知道实现的原理,但是我们使用这个功能的时候不需要考虑实现这个功能的具体细节。采用封装的思想保证了内部数据的完整性,让使用该类的用户不能轻易的直接操作数据结构,只能执行类中允许公开的数据,这样就避免了外部操作对内部数据的影响,提高了程序的可维护性。
继承:继承是类与类之间的关系的一种,这种关系主要是为了避免出现多个相类似的类。所以继承就是以现有类为基础,保存相同的属性与行为,然后扩展自己独有的特色。如同我们要构建漫画书、言情小说和工具书这个几个类,当我们不使用继承的时候,我们每个类中都会有 【书名】、【作者】、【出版社】和【价格】等属性。这样导致我们做了很多重复的事情,但是使用继承这个概念之后,我们可以找到这个几个类的共同点然后向上提取成一个父类,这个例子中父类可以是 书 。我们构建的父类中,拥有【书名】、【作者】、【出版社】和【价格】这几个基本属性和 【读书】的基本行为。当我们在构建漫画书这个类的时候,我们只需要继承 书 这个类,我们就拥有了 书 这个类中的所有非私有的属性和非私有的行为,也就是漫画书成为了书的子类。需要注意的是,父类被修改会影响到所有继承这个类的子类。如果一个类没有显式的声明继承自哪个类,那么它自动的继承自 Object。
重写:继承父类之后,发现父类的某个行为不适用于当前类,从而对继承下来的行为进行改造或重建。这个操作就是重写,也有些称之为覆盖。就像 书 这个类中的读书行为是从上到下、从左至右的读,在 漫画书 这个类中 这样读书的行为是不行的。所以在 漫画书 这个类中,我们也写了一个读书的行为,这个行为是按漫画的序号来读的。这样的话,就代表着漫画书类重写了父类中的读书行为。
重载: 重载是指在一个类中同名不同参,解释就是在一个类中拥有相同的行为,但是需要的执行要求不一样。
多态:多态理解为父类的引用指向子类的对象。还是拿上面书这个类来做例子,当我们需要读书时,我们只需要调用书这个父类中的读书行为,就可以读任何书。
抽象类:abstract 是定义抽象类的关键字。在解决实际问题时,一般将父类定义为抽象类,需要使用这个父类进行继承和多态处理。在继承和多态的原理中,继承树越上方的类就越抽象,比如鸽子类继承鸟类、鸟类继承动物类等。在多态机制中,并不需要将父类初始化对象,我们只需要它的子类对象,所以在Java 中设置抽象类不可以实例化对象,因为抽象类相当于一个概念,是没有实体的东西,而抽象类的子类是一个具体的可实例化的。使用 abstract 关键字定义的方法称为 抽象方法,一个类中只要有一个抽象方法,那么这个类就必须为抽象类。在抽象类中是允许有非抽象方法的。
接口:接口是抽象类的延伸,可以将他看成一个纯粹的抽象类,接口中所有的方法都是没有方法体的。在接口中定义的方法必须被定义为 public 或 abstract 形式,其它修饰权限不被 Java 编译器认可,即使不将该方法声明为 public 形式,它也是 public,并且在接口中定义的任何字段都自动是 static 和 final 。接口的存在,解决了Java中不可多重继承的问题,因为一个类是可以实现多个接口,这样可以将所有继承的接口放置在 implement 关键字后并用逗号隔开,但这可能会在一个类中产生庞大的代码量,因为继承一个接口时需要实现接口中所有的方法。
权限修饰符:Java 中的权限修饰符主要包括 private 、public 和 protected ,这些修饰符控制着对类和类的成员以及成员方法的访问。设置为 private 时只有本类可见,设置为 protected 时 只有本包的类可以访问,设置为 public 时所有类都可以访问。注意的是,如果声明类时不是有修饰符设置类的权限,则这个类预设为包存取范围,也就是只有一个包中的类可以调用这个类的成员变量和成员方法。
this关键字:用来区分成员变量与局部变量。this.name 代表的是成员变量中的 name,而 name 代表的是局部变量。比如set 方法中的 this.name = name; 代表将局部变量赋值给成员变量。
对象的引用:引用只是存放一个对象的内存地址,并非存放一个对象,严格地说引用和对象是不同的,但是可以将这种区别忽略,如可以简单的说 book 是 Book 类的一个对象,而事实上应该是 book 包含 Book 对象的一个引用。
对象的比较:在Java 语言中有两种对象比较方式,分别是“ == ” 运算符和 equals()方法,这两个方法有着本质的区别。equals()方法是String类中的方法,用来比较两个对象引用所指的内容是否相等,而 “ == ” 运算符是比较两个对象引用的地址是否相等。
对象的销毁:每个对象都有生命周期,当对象的生命周期结束时,分配给该对象的内存地址将会被回收。但是在 Java 中拥有一套完整的垃圾回收机制,垃圾回收器会自动回收无用的但占内存的资源。但是需要注意的是,垃圾回收器只会回收由 new 操作符创建的对象。如果某些对象不是由 new 操作符在内存中获取的内存区域,需要手动调用 finalize() 方法。有一点需要明确的是,垃圾回收或 finalize() 方法不保证一定会发生,比如 Java虚拟机内存损耗殆尽时,它是不会执行垃圾回收的。
Java 语言进阶(类的高级特性)
成员内部类:在一个类中使用内部类,可以直接存取所在类私有成员变量。内部类的实例一定要绑定在外部类的实例上,如果从外部类中初始化一个内部类对象,那么内部类对象就会绑定在外部类对象上。内部类初始化方式和其它类初始化方式相同,都是使用 new 关键词,下面是一个例子:
局部内部类:内部类不仅可以在类中进行定义,也可以在类的局部位置定义,如在类的方法或任意的作用域中均可以定义内部类。代码如下:
如果需要在方法体中使用局部变量,该局部变量需要被设置为 final 类型,换句话说,在方法总定义的内部类只能访问方法中 final 类型的局部变量,这是因为在方法中定义的局部变量相当于一个常量,它的生命周期超出方法运行的生命周期,由于该局部变量被设置为 final ,所以不能在内部类中改变该局部变量的值。
匿名内部类:由于匿名内部类没有名称,所以匿名内部类使用默认的构造方法来生成 OutInterface2 对象。在匿名内部类定义结束后,需要加分号标识,这个分号并不是代表定义内部类结束,而是代表创建 OutInterface2 引用表达式的标识。代码如下:
静态内部类:在内部类前添加修饰符 static ,这个内部类就变成了静态内部类了。一个静态内部类中可以声明 static 成员,但是在非静态内部类中不可以声明 static 成员。静态内部类有一个最大的特点,就是不可以使用外部类的非静态成员,所以在程序开发中静态内部类比较少见。
内部类的继承:内部类和其它普通类一样可以被继承,但是继承内部类比继承普通类复杂,需要设置专门的语法来完成。在某个类继承内部类时,必须硬性给予这个类一个带参数的构造方法,并且构造方法的参数为需要继承内部类的外部类的引用,同时在构造方法体中使用 a.super() 语句,这样才为继承提供了必要的对象引用。代码如下:
Java 语言进阶(异常处理)
Java 的基本理念是“结构不佳的代码不能运行”。发现错误的理想时机是编译阶段,也就是在运行程序之前,但是编译期并不能找出所有的错误,余下的错误必须在运行期解决。这样就需要错误源通过某种方式,把适当的信息传递给某个接收者,接收者就知道该如何正确的处理这个问题。Java 中异常处理的目的在于通过使用少数目前数量的代码来简化大型、可靠的程序的生成。异常带来的另一个好处就是,它可以将正常运行的程序该执行什么代码与出错后该执行什么代码进行分离,可读性与逻辑更加清晰。需要注意的是,抛出异常的时候,异常处理系统会按照代码书写的顺序找到 “ 最近 ” 的处理程序。找到匹配的处理程序之后,它就会认为异常将得到处理,然后就不会再继续查找。查找的时候并不要求抛出的异常同处理程序的异常完全匹配,派生类的对象也可以匹配其基类的处理程序。
应该在下列情况下使用异常:
1)在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常)
2)解决问题并且重新调用产生异常的方法。
3)进行少许修补,然后绕过异常发生的地方继续执行。
4)用别的数据进行计算,以代替方法预计会返回的值。
5)把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
6)把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
7)终止程序
8)进行简化。(如果你的异常模式使得问题变得太复杂,那用起来会非常痛苦)
9)让类库和程序更安全。(这既是在为调试做短期投资,也是为程序的健壮性做长期投资)
Java标准异常:Throwable 这个Java类被用来表示任何可以作为异常被抛出的类。Throwable 对象可分为两种类型(指从 Throwable继承而得到的类型):Error 用来表示编译时和系统错误(除特殊情况外,一般不需要你关心);Exception 是可以被抛出的基本类型,在Java类库、用户方法以及运行时谷正中都可能抛出Exception型异常。所以Java程序员关心的基类型通常是Exception。
Java 语言进阶(容器)
如果一个程序只包括固定数量的且生命期都是已知的对象,那么这是一个非常简单的程序。
关 于 容 器 的 深 入 研 究 在 本 文 后 部 分 !
提到容器容易让我们联想到数组,集合类于数组的不同之处是,数组的长度是固定的,集合的长度是可变的。数组用来存放基本类型的数据,集合用来存放对象的引用。常用的集合有 List集合、Set 集合和 Map 集合,其中 List 与 Set 继承了 Collection 接口,各接口还提供了不同的实现类。上述集合类的继承关系图如下:
基本概念:
Java 容器类类库的用途是“保存对象”,并将其划分为两个不同的概念:
1)Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List 必须按照插入的顺序保存元素,而Set 不能有重复元素。Queue 按照排队规则来确定对象产生的顺序(通常与他们被插入的顺序相同)。Queue 的具体实现和使用在并发中会提及。
2)Map。一组成对的“键值对”对象,允许你使用键来查找值。ArrayList允许你是用数字来查找值,因此在某种意义上,它将数字与对象关联在一起。映射表允许我们使用另一个对象来查找某个对象,它也被称为 “关联数组” ,因为它将某些对象与另外一些对象关联在一起;或者被称为 “字典” ,因为你可以使用键对象来查找值对象,就像在字典中使用单词来定义一样。Map是强大的编程工具。
List集合:
List 承诺可以将元素维护在特定的序列中。List 接口在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移除元素。有两种常用的List 实现类:
1)ArrayList 类实现了可变的数组,允许保存所有元素,包括 null ,并可以根据索引位置对集合进行快速的随机访问;缺点是向指定的索引位置插入对象或删除对象的速度比较慢。
2)linkedList 类采用链表结构保存对象。这种结构的优点是便于向集合中插入和删除对象,需要向集合中插入、删除对象时,使用linkedList 类来实现的List 集合效率较高;但对于随机访问集合中的对象,使用 linkedList 实现 List 集合的效率较低。
Set集合:
Set 集合中的对象不按特定的方式排序,只是简单的把对象加入集合中,但 Set 集合中不能包含重复的对象。Set 集合由Set 集合与Set 接口的实现类组成。Set 接口继承了 Collection 接口,因此包含了 Collection 接口的所有方法。Set 接口常用的实现类有:
1)HashSet 类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 Set 的迭代顺序,特别是它不保证该顺序恒久不变。此类允许使用 null 元素。
2)TreeSet 类不仅实现了 Set 接口,还实现了 java.util.SortedSet 接口,因此,TreeSet 类实现的 Set 集合在遍历集合时按照自然顺序递增排序,也可以按照指定比较器递增排序,即可以通过比较器对用 TreeSet 类实现的 Set 集合中的对象进行排序。TreeSet 类新增的方法如下表:
| 方法 | 功能描述 |
|---|---|
| first() | 返回此 Set 中当前第一个(最低)元素 |
| last() | 返回此 Set 中当前最后一个(最高)元素 |
| comparator() | 返回对此 Set 中的元素进行排序的比较器。如果此 Set 使用自然排序,则返回 null |
| headSer(E toElement) | 返回一个新的 Set 集合,新集合包含 toElement(不包含)之前的所有对象 |
| subSet(E fromElement,E fromElement) | 返回一个新的 Set 集合,包含 fromElement(包含)对象与 fromElement(不包含)对象之间的所有对象 |
| tailSet(E fromElement) | 返回一个新的 Set 集合,辛几何包含对象 fromElement(包含)之后的所有对象 |
Map集合:
Map集合没有继承 Collection 接口,其提供的是 key 到 value 的映射。Map 中不能包含相同的 key,每个 key 只能映射到一个 value。key 还决定了存储对象在映射中的存储位置,但不是由 key 对象本身决定的,而是通过 “散列技术” 进行处理,产生一个散列码的整数值,散列码通常作为一个偏移量,该偏移量对应分配给映射的内存区域的起始位置,从而确定存储对象在映射中的存储位置。Map 集合包括 Map 接口以及 Map 接口的所有实现类。Map 接口常用的实现类有:
1)HashMap 类是基于哈希表的 Map 接口的实现,此实现提供所有可选的映射操作,并允许使用 null 键和 null 值,但必须保证键的唯一性。HashMap 通过哈希表对其内部的映射关系进行快速查找。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2)TreeMap 类不仅实现了 Map 接口,还实现了 java.util.SortedMap 接口,因此,集合中的映射关系具有一定的顺序。但是在添加、删除和定位映射关系时,TreeMap 类比 HashMap 类性能稍差。由于 TreeMap 类实现的 Map 集合中的映射关系是根据键对象按照一定的顺序排列的,因此不允许键对象是 null 。
建议使用 HashMap 类实现 Map 集合,因为 HashMap 实现的 Map 集合添加和删除映射关系效率更高,HashMap 通过哈希码对其内部映射关系进行快速查找。可以通过 HashMap 类创建 Map 集合,当需要顺序输出时,在创建一个完成相同映射关系的 TreeMap 类实例就好了。
Java 语言进阶(Java I/O 系统)
在变量、数组和对象中存储的都是暂时存在的,程序结束之后他们就会丢失。为了能够永久的保存程序创建的数据,需要将其保存在磁盘文件中,这样就可以在其它程序中使用它们。Java 的 I/O 技术可以将数据保存到文本文件、二进制文件甚至是 ZIP 压缩文件中,以达到永久性保存数据的要求。
流概述:流是一组有序的数据序列,根据操作的类型,可以分为输入流和输出流两种。I/O,就是 输入(Input) 和输出( Output)。I/O流提供了一条通道程序,可以使用这条通道把源中的字节序列送到目的地。虽然 I/O 流通常与磁盘文件存取相关,但是程序的源和目的地也可以是键盘、鼠标、内存或者显示器等等。可以把 I/O 传输看成一条管道。具体模式图如下:
FIle类:
File(文件)类这个名字有一定的误导性,不单单只代表文件,还可以代表一个目录下的一组文件的名称。实际上,FilePath(文件路径)对这个类来说是个更好的名字。File类是 java.io 包中唯一代表磁盘文件本身的对象。可以通过File类实现创建、删除、重命名文件等操作。File 类的对象主要用来获取文件本身的一些信息,比如文件所在目录、文件的长度、文件读写权限等。数据流可以将数据写入到文件中,文件也是数据流最常用的数据媒体。
输入和输出:
在编程语言的I/O类库中常使用 流 这个抽象概念,它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。流 屏蔽了实际的I/O设备中处理数据的细节。通过继承,任何 Inputstream 或 Reader 的子类都含有 read() 这个基本方法,用于读取单个字节或字节数组。同样任何 OutputStream 或 Writer 的子类都含有 write() 方法来写单个字节或字节数组。但是我们通常不会使用这些方法,他们只所以存在是因为别的类可以使用它们,以便于提供更为有用的接口。因此,我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这就是装饰器设计模式,之后会讲到)。实际上,Java中“流”让人迷惑的主要原因在于:创建单一对象的结果流,却需要创建多个对象。
Reader和Writer:
Inputstream 和 OutputStream 在字节形式的I/O中提供了极有价值的功能,而 Reader 和 Writer 则提供兼容 Unicode 与面向字符的 I/O 功能。当然有时候我们需要把字节结构中的类和字符结构中的类结合起来使用,为了实现这个目的,就要用到适配器类(adapter):InputStreamReader 可以把 InputStream 转换为 Reader,而 OutputStreamWriter 可以把 OutputStream 转换为 Writer。设计 Reader 与 Writer 继承层次结构主要是为了国际化。Unicode 用于字符国际化,它是双字节的,所以添加 Reader 和 Writer继承层次结构是为了在所有的 I/O 操作中都支持 Unicode。
自我独立的类(RandomAccessFile):
RandomAccessFile 适用于由大小已知的记录组成的文件,所以我们可以使用 seek() 将记录转移到别处,然后读取或者修改记录。文件中记录的大小不一定都相同,只要我们能够确定那些记录有多大以及它们在文件中的位置就好了。说它自我独立是因为 RandomAccessFile 没有继承 I/O的类,它只实现了 DataInput 和 DataOutput 两个接口。这么做的原因是因为 RandomAccessFile 拥有和别的 I/O 类型本质不同的行为,因为我们可以在一个文件内向前和向后移动。也只有 RandomAccessFile 支持搜寻方法,并且只适用于文件。
新I/O:
JDK 1.4的java.nio.*包中引入了新的Java I/O类库,目的是提高速度。实际上,在旧的 I/O 包已经使用 nio 重新实现过,以便充分的利用这种速度的提高,因此,即使我们不显式的用 nio 编写代码也能从中受益。速度的提高在文件 I/O 中和 网络 I/O 中都有可能发生。速度的提高来自于所使用的结构更接近与操作系统执行 I/O 的方式:通道和缓冲器。nio 修改了三个类,分别是 FileInputStream、FileOutputStream以及读写都支持的 RandomAccessFile。
字节存放次序:
不同的机器可能会使用不同的字节排序方法来存储数据。“big endian”(高位优先)将最重要的字节存放在地址最低的存储器单元。而“little endian”(低位优先)则是将最重要的字节放在地址最高的存储器单元。只要存储量大于一个字节时,就需要考虑字节的顺序问题了。ByteBuffer 是以高位优先的形式存储数据的,并且数据在网上传送时也常常使用高位优先的形式。我们可以使用带有参数 ByteOrder.BIG_ENDIAN 或 ByteOrder。LITTLE_ENDIAN 的 order() 方法改变 ByteBuffer 的字节排序方式。下面举个例子:
如果我们以 short(ByteBuffer.asShortBuffer())形式读取数据,得到的数字是 97(二进制形式为 00000000 01100001);但是如果将ByteBuffer 更改为低位优先的形式,再来用 short形式读取数据,得到的数字却是 24832(二进制形式为 01100001 00000000)。
缓冲器的细节:Buffer 是由数据和可以高效访问及操纵这些数据的四个索引组成,这四个索引分别是:mark(标记),limit(界限),position(位置)和capacity(容量)。
内存映射文件:
内存映射文件允许我们创建和修改那些因为太大而不能完全放入内存的文件。有了内存映射文件,我们就可以假定整个文件都放在了内存中,而且可以完全把它当作非常大的数组来访问。这种方法极大的简化了用于修改代码的时间。下面是一个小例子:
为了既能写又能读,我们先由 RandomAccessFile开始,获得该文件上的通道,然后调用map()产生MappedByteBuffer,这是一种特殊类型的直接缓冲器。需要注意的是,我们必须指定映射文件的初始位置和映射区域的长度,这意味着我们可以映射某个大文件的一小部分。
文件加锁:
添加了加锁机制后,它允许我们同步访问某个作为共享资源的文件。不过竞争同一文件的两个线程可能在不同的Java虚拟机上,或者一个是Java线程另一个是本地操作系统中的其它线程。文件锁对其它操作系统进程是可见的,因为Java的文件加锁是直接映射到了本地操作系统的加锁工具。通过 FIleChannel调用tryLock()或 lock() 方法,就可以获得整个文件的 FileLock(Socket-Channel、DatagramChannel和ServerScoketChannel不需要加锁,因为它们是从单进程实体继承而来,通常我们不在两个进程之间共享网络socket)。获取 文件锁的两个方法中,tryLock()是非阻塞的,它如果没有或得到锁(也就是已经有其他进程已经持有相同的锁并不共享时)它将直接从方法调用返回。lock()则是阻塞的,它要获取到锁才会返回,或者调用lock()的线程中断,或者调用lock()的通道关闭了。文件锁也可以选择全上锁和部分上锁,如果部分上锁,文件大小超过设定的上锁区域,那么超出的文件部分不会被上锁。如果全上锁,无论文件大小怎么变化都是上锁的。还需要注意的是,对独占锁或者共享锁的支持必须由底层的操作系统提供。如果操作系统不支持共享锁并为每个请求都创建一个锁,那么它就会出现独占锁。锁的类型(共享或独占)可以通过FileLock.inShared()进行查询。关于映射文件的部分加锁,文件映射通常应用于极大的文件。我们可能需要对这种巨大的文件进行部分加锁,以便于其它进程可以修改文件中未加锁的部分。这种典型的代表就是数据库,所以数据库的多个用户可以同时访问它。
Java 语言进阶(反射)
在了解反射机制之前需要先了解一个解决方案,这种解决方案被称为潜在类型机制。潜在类型机制是Java泛型思想的一种实现方式,一般的类和方法只能使用具体的类型:要么是基本类型、要么是自定义的类。如果要编写可以运用到多种类型的代码,这种刻板的限制对代码的束缚就会很大。在面对对象的语言当中,多态算一种泛化机制。有时候拘泥于单继承体系也会使程序受限太多,如果方法的参数是一个接口而不是一个类,这种限制就会放松很多。因为任何实现了这个接口的类都能够满足该方法,这也包括了暂时还不存在的类。这就给我们客户端程序员一种选择,他可以通过实现一个接口来满足类或方法。因此,接口允许我们快捷的实现类继承,也使我们有机会创建一个新类来实现这一点。
可是有时候即使我们使用了接口,对程序的的约束还是太强了。因为一旦指定了接口,它就要求你的代码必须使用指定的接口。而我们希望达到的目的是编写更通用的代码,要使代码能够应用与“某种不具体的类型”,而不是具体的接口和类。这就是Java SE5 的重大变化之一:泛型的概念。泛型实现类参数化类型的概念,可以使代码应用与多种类型。“泛型”这个术语的意思是:适用于许多许多的类型。泛型在编程语言中出现时,其最初的目的是希望类或方法能够具备最广泛的表达能力。为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所作的限制,同时不丢失静态类型检查的好处。Java泛型看起来使是向这个方向靠近了一步,当你编写或使用只是持有对象的泛型时,这些代码可以运用与任何类型(除了基本类型,自动包装机制可以克服这一点)。如果代码可以不关心它要作用的类型,那这种代码就可以真正的运用于任何地方,并以此而相当“泛化”。
”潜在类型机制“这种解决方案也被称为“结构化类型机制”,还有一个更奇怪的名字“鸭子类型机制”。也就是“如果它走起来像鸭子,并且叫起来也像鸭子,那么你就可以把它当鸭子对待。”具有潜在类型机制的语言只要求实现某个方法的子集,而不是一个特定的类或接口,从而放松了这种限制(并且可以产生更加泛化的代码),正因如此,潜在类型机制可以横跨类继承结构,调用不属于某个公共接口的方法。通俗来说就是:“我不关心你是什么类型,只要你可以speak()和sit()即可。”由于不要求具体类型,因此代码就可以更加泛化。潜在类型机制是一种代码组织和复用机制。代码组织和复用是计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。因为我并未被要求我的代码要操作于其上的确切接口,所以有了潜在类型机制我们就可以编写更少的代码,并更容易的将其应用与多个地方。
两种支持潜在类型机制的语言实例是Python和C++。Python是动态类型语言(事实上所有类型检查都发生在运行时),而C++是静态类型语言(类检查发生在编译期),因此潜在机制类型不要求静态或动态类型检查。
我们将上面的描述用Python来表示,表示如下:
#: generics/DogsAndRobots.py Class Dog: def speak(self): print "Arf!" def sit(self): print "Sitting" def reproduce(self): pass Class Robot: def speak(self): print "Click!" def sit(self): print "Clank!" def oilCHange(self): pass def perform(anything): anything.speak() anything.sit() a = Dog() b = Robot() perform(a) perform(b) #:~
根据上面的代码我们可以注意到,perform()方法的参数 anything 并没有指定具体的类型,anything 只是一个标识符,它必须能够执行 perform()期望它执行的操作,因此这里隐含着一个接口。但是你从来都不必显式的写出这个接口——它是潜在的。preform()不关心其参数的类型,因此我们可以向它传递任何对象,只要对象支持speak()和sit()方法。如果传递给perform()的对象不支持这些操作,那么就会得到运行时异常。如果我们试图用Java来实现上面的示例,那么就会被强制要求使用一个类或接口,并在边界表达式中指定它,尽管Java不支持潜在类型机制,但是这并不意味着有界泛型代码不能再不同的类型层次结构之间运用。反射就是对缺乏潜在类型机制的补偿的一种,我们仍旧可以创建正在的泛型代码,但是这需要付出一些额外的努力。下面是使用反射是来实现潜在类型机制:
import java.lang.reflect.Method;
//Does not Implement Performs;
class Mime {
public void walkAgainstTheWind() {
}
public void sit() {
System.out.println("Pretending to sit");
}
public void pushInvisibleWalls() {
}
public String toString() {
return "Mime";
}
}
//Does not Implement Performs;
class SmartDog{
public void speak(){
System.out.println("Woof!");
}
public void sit(){
System.out.println("Sitting");
}
public void reproduce(){}
}
class Robot{
public void speak(){
System.out.println("Click!");
}
public void sit(){
System.out.println("Clank!");
}
public void oilChange(){}
}
class CommunicateReflectively{
public static void perform(Object speaker){
Class> spkr = speaker.getClass();
try {
try {
Method speak = spkr.getMethod("speak");
speak.invoke(speaker);
}catch (NoSuchMethodException e){
System.out.println(speaker + " cannot speak");
}
try {
Method sit = spkr.getMethod("sit");
sit.invoke(speaker);
}catch (NoSuchMethodException e){
System.out.println(speaker + " cannot sit");
}
}catch (Exception e){
throw new RuntimeException(speaker.toString(),e);
}
}
}
public class LatentRefletion{
public static void main(String[] args) {
CommunicateReflectively.perform(new SmartDog());
CommunicateReflectively.perform(new Robot());
CommunicateReflectively.perform(new Mime());
}
}
//Output:
Woof!
Sitting
Click!
Clank!
Mime cannot speak
Pretending to sit
上例中,这些类完全是彼此分离的,没有任何公共基类(除了Object)或接口。通过反射, CommunicateReflectively.perform()能够动态的确定所需要的方法是否可用并调用它们。它甚至能够处理Mime只具有一个必需的方法这一事实,并能够部分实现其目标。
Java反射机制可以使程序员更加深入的控制程序的运行过程。包括之后的框架以及Annotation功能都是建立在反射的基础之上。
通过反射实例可以将程序中访问已经装载到JVM中的Java对象描述,可以实现访问、检测和修改描述Java对象本身信息的功能。Java反射体系的功能十分的强大,在 java.lang.reflect包中提供了对这些功能的支持。
在反射中执行具有可变数量的参数的构造方法时,需要将入口参数定义成二维数组。
参考书籍:Thinking in Java 第四版



