Java编程思想第4版-不完全详解

716 阅读43分钟

封面7 拷贝.png

作者:老九—技术大黍

产品:查看原文

社交:知乎

公众号:老九学堂(新手有福利)

特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权

前言

记得98年大学毕业后,入行找工作很难啊,绝对比现在的大学生找工作难多了。虽然在学校是计科系毕业,但是招聘的公司时要求的全部都是些VC++、Delphi、Java(那个时候Java是最新、最时髦的)、VB、Oracle数据库、Weblogic、EJB等这些岗位招聘(那个时候像Tomcat这种Servlet容器是根本拿不上台面啊~),并且全部都要求是一年以上工作岗位啊image-20210322141122987.png~

这些岗位技术我在学校都是没有学习过的,我记得学校里学的应用开发技术是数据库DBase 3,像VB、VC++、Delphi、Java这些应用开发语言木有,学校里关于计算机编程的只学习过Pascal、Fortran、汇编、C、8086/8088单片机编程,还就是反编译原理、操作系统原理、计算机组成原理、模拟电路 、数字电路等等。我在毕业的半年实习期中使用DBase3数据库给一家银行开发过人事报表系统和办公室水电收费系统。

我曾经以为应用开发就是使用数据库来开发,因为数据库脚本很简单啊,有点像原始英文语法,什么select...from...where这种格式的,如果我们加一个主语I,那么就组成了I select/insert/update/delete... from ... where...in.. 的语法格式了。大家是不是会发现,数据库脚本真的是非常简单嘛?我个人认为只要初中英语过关,谁都可以使用数据库来开发应用程序啊。

那个时候的财会系学生都要学习数据库编程的,这进一步证明了,其实数据库真的很简单啊(呵呵,不过好像现在世道变了一样,现在很多人觉得数据库很难学,书写一全SQL语句像会要他们的命似的image-20210322143550691.png

那个时候还没有像现在有各种培训机构给毕业生充电,我花了一周时间自学习VB,然后找了平生的第一份工作,接着开始自学Java后跳槽到上海一家外企做EPR项目的开发。在上海期间,平生非常有幸遇到我的组长X辉(复旦计科系毕业,他真实的姓我隐去了)。他是一位牛人,使用JDK 1.4版的Swing技术为我们公司50个Java程序员书写一个excel应用来处理各种公文(因为在外企都是用正版的,老板不愿意给我们正版Office用,因此我们老大自己写了一个Excel应用程序!),这款应用程序非常好用,各种设计模式用得真是一个牛批个拉斯!

当时的我看不懂大佬书写的代码啊,心里急得像热锅上的蚂蚁啊。最后没有办法,于是就厚着脸皮去问老大:“组长,我需要看什么书才能为Java高手啊?”

“Thinking in Java!”我们牛人辉组长只甩给我这样一句话。

听完之后,我就跑到公司的KF服务器上找到了《Thinking in Java Second Edition》一书,接着就是像读圣经一样研读起来了...

我们老大没有骗我:一本书好让人受益终生!

当然,一本坏书毁人青春!image-20210322143115069.png

多年过去了,如果人有问我:怎么才能学精Java?我会回答他:首先读“Thinking in Java”,然后再读“Core Java”。这两本书,足矣!

今天我解读的是《Thinking in Java Fourth Edition》(Java编程思想第四版)一书。该书的作用是我的偶像:Bruce Eckel--一个嵌入式专业毕业的牛人、程序员和MindView公司的CEO。

image-20210322151056633.png

注意:他是标准C++委员会最初发起的成员--参与制作C++标准的成员,我相信这种牛批的程度,不用我再累述了吧。

他出过的经典书籍如下:

  • Thinking in Java系列
  • Thinking in C
  • Thinking in C++
  • Thinking in Python

本人除了《Thinking in Python》没有看过,其他我都研读过。也正是因为拜读过Bruce Eckel的大作,并且有10年的Java开发经验,所以我才会说自己一个血统纯正的Java程序员。

当然,偶的C++也不赖啊--5年C++手机游戏开发经验,比Java差那么一丢丢image-20210322143550691.png

好了,咱们言归自传吧。

作为计科系出身的我,下面的翻译只能当成一种参考,请大家不要当成标准。如果有不足之处,请大家指正和补充。希望能够帮助大家快速理解Bruce Eckel的想法。

对象简介

image-20210322151850878.png

我是这样看待Java的:它试图把程序员从操作系统硬件层级的理解剥离开,并且引导程序员从原来必须理解硬件的,现在只需要理解软件概念即可,即成为一个纯“软件人”。

image-20210322155142258.png

“software craftsman”还可以翻译成“纯软件程序员”,或者是“码农”image-20210322141122987.png。理解大牛人有鄙视Java的意思~ 当然,他也说出了Java生产的动机--即让没有C/C++基础的人也可以学习会编程。

当今现状已经证明Java的成功之处:我们刚入门学习Java时,确实不需要知道什么叫计算机组织原理、操作系统原理等等这些底层的东西。所以,现在Java成为了什么人都可以学习的编程语言,同时也是它为什么在C++之后,一跃超过C++成为全球使用最多的编程语言。因为它只学习一种思想--面向对象编程!

面向对象编程--Object Oriented Programming(简称OOP)。

Java是什么?

image-20210322153218558.png

OOP编程的动机是把计算机作为表述的媒体来使用(Object-oriented programming is part of this movement toward using the computer as an expressive medium)。

所以,Java就是把计算机作为表述的媒体的语言。

抽象的过程

大多数人在使用面向对象的方式编程时不会产生理解原不适应,所以,我们需要讲解为什么会这样在,因为与人类的思维抽象有关。 编程语言提供的抽象,它可以解决类型与数量的抽象,对于”kind”的意思就是:”What is it that you are abstracting?” 汇编语言是底层机器的抽象,所谓“imperative(命令)”语言,比如FORTRANT, BASIC和C语言是汇编语言的抽象。这些语言是对汇编语言的提高,但是关键抽象(primary abstraction)还是需要程序员使用计算机指令来表示,而不是你试图解决的问题指令(the structure of the problem)。这时期的程序员必须建立这样的关联—机器模型(即“解决空间”中就是实现解决的地方,比如计算机)和问题域,也就是实际需要解决的问题(即“问题空间”—指实际问题存在的地方,比如商业事务逻辑)。”The programmer must establish the association between the machine mode (in the “solution space”, which is the place you’re implementing that solution, such as a computer) and the model of the problem that is actually being solved (in the “problem space”, which is the place where the problem exists, such as business)”. 这导致必须完成这样的影射,而这样的影射实际是编程语言无关,并且结果就是程序非常难书写与维护,副作用就是创造了一个“编程方法” (programming methods)工业。

面向对象的方式给程序员提供了一种工具来表示问题空间(problem space)中的元素。这种表述是宽泛的(general)的表述,它满足了程序员一般的表示要求,而不是陷于特定问题的约束。我们把在问题空间中的元素和它们在解决空间(solution space)中的表示叫做“对象(objects)”(当然你也可以使用问题空间之外的对象)。因为这种思想,那么程序本身就隐喻了问题,这种隐喻的方式是通过添加新的对象类型来表示!这样我们阅读代码来描述解决方案时,你阅读的单词也就表示了问题。由此看来,OOP编程就是允许你使用问题术语本身来描述问题,而不是使用计算机术语来表示。这里与计算机有一点联系:每个对象就象一个小的计算机—它有状态,它有操作你可以请求!所以这一点上,对象与真实的对象不一样—它们都有属性与行为。

如果想要进行纯粹的面向对象的编程,那么需要满足下的特点:

  • Everything is an object. 我们需要把一个对象看成一个奇特的变量;该变量存贮数据,并且你可以向该对象“发送请求”,要求它实现一个操作。理论上讲,你可以使用任何概念上的组件来表示试图解决的问题(比如dogs, buildings, services等),然后把这些抽象的概念表示成程序中的对象。
  • Each object has its own memory made up of other objects. 我们通过把现存的对象放到一个包中,然后能够创建出新的对象来。这样,你可以把复杂的程序隐藏在一个简单的对象之中。
  • Every object has a type. 在描述上,每个对象是一个类的实例,而“class”与”type”的语义上是一样的。最重要的是,对于一个class来说,我们需要关注:“What messages can you send to it?”
  • A program is a bunch of objects telling each other what to do by sending messages. 如果需要向一个对象发送请求,那么你需要“send a message”给该对象。确切的说,你可以认为一个请求消息就是呼叫一个属于特定对象的方法!
  • All objects of a particular type can receive the same messages. 这句话的意思实际上是一个被装载的语句。因为一个“circle”类型对象也是“shape”类型的对象,那么一个circle保证可以接收shape的消息。这就是说,你可以书写代码来告诉shape自动的处理任务事情,这些事情符合一个shape描述的要求的东西。这概念是OOP中最强大的概念!!!

**注:**Booch关于对象提供更为简洁的描述如下:“An object has state, behavior and identity.” 意思就是,一个对象有内部数据、方法,并且每个对象是唯一的,也就是在内存中有唯一地址。

有接口的对象

Aristotole(亚里士多得)可能是第一个仔细研究类型概念的人(Aristotle was probably the first to begin a careful study of the concept of type); 他说“the class of fishes and the class of birds.” 这句话的意思是:所有对象是唯一的,而且它们是对象类型的一部分,并且这些对象有特性和行为。这种思想直接被使用第一代面向编程的语句Simula-67语句上,该类语句最基本的关键字”class”在程序中表示了一种新的类型(type)。

Simula语句名字暗示了,我们模拟古典的“银行出纳问题(bank teller problem)”。我们有很多出纳、客户、帐户、事务和货币单元—它们都是一大堆“对象(objects)”。对象在执行是唯一的,此外它们被规为某类东西”classes of objects (对象类别)”,这也就是关键字class的由来!

创建抽象数据类型(classes)是面向对象编程的基本概念,抽象数据类型(abstract data types)实际上有点象内置类型:你可以创建一个类型的变量(我们叫对象或者实例)和操作这些变量(呼叫发送的消息或者请求;你可以向一个消息给一个对象,然后告诉该对象需要做什么)。

每个类的成员(元素)是被共享的:比如每个帐户都有一个余额,每个出纳都可以接受存款等。与此同时,每个成员有自己的状态:每个帐房有不同的余额值;每个出纳有自己的名字等。因此,出纳、客户、帐户和事务在计算机中都有一个唯一的实体来表示!这个实体就是一个对象,而每个对象属于一个特定的类,类是用来定义对象的特性和行为的。

因此,虽然我们使用面向对象的编程的工作就是创建新的数据类型,但是,实际上我们使用”class”关键来完成这些动作。当你看见单词“type”时,你可把它转换成”class”,反之即然。

因为一个类描述了一组对象,这些对象有相同的特性(数据元素)和行为(功能),所以一个类是真正的数据类型。那么对于程序员来说,定义类来描述问题而不使用直接使用计算机中的存贮单元描述的数据类型,它们的不同之处是,我们可以通过添加新的数据类型来满足问题描述。编程系统欢迎新的类,但是需要小心处理这些类型与内置类型的关系!

面向对象方法不只限于实现模拟的动作,不管你是否同意一个程序就是你设计系统的模拟器的观点,我们使用OOP技术时,可以大大简化实现问题的解决困难。

image-20210322154851297.png

上图描述了一个对象可以满足实际的请求,向一个对象发送一个请求的条件是,该对象必须有自己的接口(interface), 并且由类型来决定该接口。因此,我向灯泡请求点亮的语句如下:

Light light = new Light(); //使用类定义一个灯泡

light.on(); //请求light执行点亮的动作

接口on()决定了你请求特定对象list的响应结果。然而,我们的代码必须在某处满足这种请求。这样可以使用隐藏数据,也就是包含了“实现”的概念。从面向过程的语句观点讲,它不是那么复杂。一个类型有方法处理各种可能的请求,当你请求向对象发送一个特定请求时,该方法被呼叫。该过程一般被叫做“发送一个消息”(请求)给一个对象然后该对象根据该消息来响应(执行相应的代码)。

image-20210322155346687.png

提供服务的对象

当我们开发或者需要理解一个程序设计时,最好的方式就是把对象看成是“服务提供者(service providers)”。你的程序会提供服务给用户(包括客户端程序员和最终用户等),而这些对象要完成这些服务时,会使用其它相应的对象来实现。你的目标是生产(最好在现有代码库中基础生产)一组对象,它们提供了解决现实问题的理想的服务!

这种方式的开启需要这样问“如果可以像变魔术一样把它们从一个帽子里拿出来,那么对象可以正确解决我的这个问题吗?(If I could magically pull them out of a hat, what objects would solve my problem right away?)”. 比如,假设我们开发一个记事本程序,那么你可能会想像一些对象它包含了预定好的纸簿在输入的屏幕,而另外一些对象执行这些纸薄的计数工作,还有一些对象处理不同打印机的打印动作等。而这些东西可能有的有现成的,而有的东西没有,那么这些没有的东西(对象)—它们应该是怎样的状况呢?这些对象提供什么样的服务呢,这些对象需要它们怎样实现这些服务呢?如果你具有这样的思想,那么你可以确定“那个对象似乎可以满足之些要求”,或者“我确定该对象已经有了”—这种方式是非常合理的方式,它可以把问题切割成一组对象。

把对象想像成一个服务的提供者还有另外一个好处:它可以帮助我们改善对象的内聚性(cohesiveness)。因为高内聚性是软件设计的基本要求:它表示一个软件组件的各个方面(比如一个对象,虽然这也可能应用一个对象库或者一个方法上)会”fit together” well. 设计上常出现的问题就是,把一个对象设计有太多功能的对象。比如,当你设计支票打印模块时,你可以让一个对象需要知道所有打印格式的功能,以及实现所有打印机器打印的功能。如果你发现这样对于一个对象的功能太多了,那么你需要把它们切割成三个或者更多个对象来处理。这样的设计是高内聚性的设计!比如,一个对象完成支票格式的检验,一个(缚)对象完成通用打印机的接口—它知道各种不同的打印机,第三个对象使用这些对象提供的服务完成任务。因此,一个好的面向对象的设计是:每个对象只做一条事情,而不要做更多的事情!!!!

image-20210322164048640.png

对象隐藏实现细节

如果把对象创建的使用的场景分成两部分:class创造者(创建新的数据类型)和客户端程序员client programmer (类的消费者,他们在应用中使用这些数据类型),那么对于理解对象来说是非常有帮助的。对于客户端程序员来说,他的目标就是收集大量的工具类,然后能够使用它们快速的进行应用开发。而类创造者的目标是—只创建类,然后把客户端需要的部暴露出来给客户端程序员,而把其他实现细节隐藏起来。为什么?因为,如果隐藏起来之后,客户端程序员就不能访问了,这样类的创造者不用担心在修改了隐藏部分之后,会影响客户端的使用。这样,隐藏实现细节可以减少程序的bug!!!

重用对象的实现细节

如果一个类被创建之后,并且经过测试,那么从理论上说它表示一个有用的代码单元。这样做产生了这样的情况—重用并不像大多数希望的那样容易使用;它需要开发人员有经验和理解怎样使用可重用的对象设计。但是,如果你有这样的设计,那么必须希望被重用。代码重用是面向对象编码语言提供的最强大的优点。

重用一个类的最简单的方式就是直接使用该类的对象,但是你也可以把这引类放到新创建的类中去使用。我们叫这种方式为“creating a member object(创建一个成员对象)”。如果你创建的类是由其它对象组成的,那么你可以使用任何方式在新创建类的来联合这些对象来完成相应的功能。因为,你是使用从现有类来组成新的类,那么这个概念被叫做“composition(组合)”—如果该组合是动态发生的,那么我们叫它为aggregation(聚合)。组合一般表示“has-a”的关系,比如“An car has an engine”。

image-20210322164254852.png

组合可以带来处理的灵活性,因为新创建类的成员对象一般是私有的,所以对于客户端程序员来说不可以访问的。那么对于类创建者来说,可以修改这些成员变量,而不影响现有的客户端代码的分布。同时,因为你可以在运行时动态的修改成员对象,所以灵活性很好。而如果是继承,那么没有这种扩展性,因为编译时编译器会检查继承结构的中的所有类型的问题。

对象的继承

就对象本身来说,理想状态的对象就是一个方便的工具!它允许你使用各种概念(concept)来打包数据和功能,所以你可以使用对象来表示一个问题空间中相应的问题,而不像C一样使用底层的机器语言来表示问题空间。这些概念是通过使用关键字class来表示的,并且这些概念是编译语言最基本的单元。

无标题.png

对于继承父类的子类,如果想扩展父类的功能最简单的方式就是添加原父类没有的方法;但是这不是好的方式,因为这些方法父类也可能会使用到。那么,既然不能添加,我们可以想到的就是改变原父类的方法了—也就是我们使用重写(overriding)来改变父类的行为,从而也让父类可以使用子类的方法了。所以,上图中Shape修改如下

无标题1.png

代码映射-Shape类

import static java.lang.System.*;
import java.awt.Color;

/**
	功能:书写一个Shape类,用来演示继承的使用
	作者:技术大黍
	备注:它是一个抽象类,用来表示抽象概念形状
	*/
public abstract class Shape{
	protected Color color;
	
	public abstract void draw(); //画的动作
	public abstract void erase();//擦除动作
	public abstract void move(); //移动动作
	public abstract Color getColor(); //得到颜色
	public abstract void setColor(Color color); //设置颜色
}

代码映射-Circle类

import static java.lang.System.*;
import java.awt.Color;

/**
	功能:书写一个Circle类,用来演示继承的使用
	作者:技术大黍
	备注:它是一个抽象类的实现类表示园形
	*/
public class Circle extends Shape{
	
	public  void draw(){ //画的动作
		out.println("draw() method in Circle class defined");
	}
	public  void erase(){//擦除动作
		out.println("erase() method in Circle class defined");
	}
	public  void move(){//移动动作
		out.println("move() method in Circle class defined");
	}
	public  Color getColor(){ //得到颜色
		return color;
	}
	
	public  void setColor(Color color){ //设置颜色
		this.color = color;
	}
}

代码映射-Square类

import static java.lang.System.*;
import java.awt.Color;

/**
	功能:书写一个Square类,用来演示继承的使用
	作者:技术大黍
	备注:它是一个抽象类的实现类表示方形
	*/
public class Square extends Shape{
	
	public  void draw(){ //画的动作
		out.println("draw() method in Square class defined");
	}
	public  void erase(){//擦除动作
		out.println("erase() method in Square class defined");
	}
	public  void move(){//移动动作
		out.println("move() method in Square class defined");
	}
	public  Color getColor(){ //得到颜色
		return color;
	}
	
	public  void setColor(Color color){ //设置颜色
		this.color = color;
	}
}

代码映射-Triangle类

import static java.lang.System.*;
import java.awt.Color;

/**
	功能:书写一个Triangle类,用来演示继承的使用
	作者:技术大黍
	备注:它是一个抽象类的实现类表示三角形
	*/
public class Triangle extends Shape{
	
	public  void draw(){ //画的动作
		out.println("draw() method in Triangle class defined");
	}
	public  void erase(){//擦除动作
		out.println("erase() method in Triangle class defined");
	}
	public  void move(){//移动动作
		out.println("move() method in Triangle class defined");
	}
	public  Color getColor(){ //得到颜色
		return color;
	}
	
	public  void setColor(Color color){ //设置颜色
		this.color = color;
	}
}

代码映射-ShowShape类

import static java.lang.System.*;
import java.awt.Color;
import java.util.*;
import javax.swing.*;

/**
	功能:书写一个演示类,用来演示继承的使用
	作者:技术大黍
	备注:程序入口
	*/
public class ShowShape{
	
	public ShowShape(){
		//使用泛型来演示编译时的动态绑定类型
		List<Shape> list = new ArrayList<Shape>();
		demoGeneric(list);
		list.get(2).draw();//呼叫容器中对象的方法draw
	}
	
	private void demoGeneric(List<Shape> list){
		//声明几个对象circle对象
		Shape data = new Circle();
		list.add(data);
		data = new Square();
		list.add(data);
		data = new Triangle();
		list.add(data);
	}
	
	public static void main(String[] args){
		new ShowShape();
	}
}

使用多态来互换对象

我们处理类型的继承体系时, 我们一般希望一个对象当成一个通用对象来使用,而不是一个特定对象来使用。这样的结果是,我们书写的代码不需要依赖于特定类型。这样做的另外一个好处就是—当我们添加新的类型时,不会影响基类对象的使用—从而大大减少了软件的维护成本。

但是,这样做的有一个问题,那就是把被驱动类型的对象是通过泛型类来实现的。比如,我们试图使用一具泛型的shape来画自己,或者一个泛型的交通工具来掌舵,或者让一个泛型的鸟来飞等,这时编译不能知道编译时真正哪段代码需要被执行。

如果我们不知道哪段代码会被执行,那么你不得不添加新的子类,而添加的代码必须与别的方法不一样,否则,编译器不能够准确知道到底哪段代码会被执行,那么如果我们需要解决这样的问题怎么办?

image-20210322165550083.png

在非OOP编译器中会使用叫做early binding(前期绑定)的技术来决定哪个鸟类来飞,而在OOP编译器中会使用叫做late binding(后期绑定)的技术来实现。当我们向一个对象发送一个消息时,那么该方法中的代码会在运行时被确定。OOP编译器不能确保该方法的存在,以及不执行参数类型和返回值的检查动作,因为它不知道实际执行的代码。为实现后期绑定动作,Java在实际呼叫的场所使用特别的代码信息,所以,每个对象可使用这些特殊的标记信息来与其它对象区别开来。所以,当我们向一个对象发送消息时,该对象知道它应该做什么—执行实际的代码。

单继承

自从C++出现之后,OOP编程变得非常的流行和重要,在C++编程中程序员需要创建自己的单继承对象树来解决问题域。而在Java本身就是一个纯粹的OOP语言,它根据这种需求使用了单继承方式的对象树—所有对象都继承Object类—从而给广大的OOP程序带来非常大的方便。C++没有使用单继承是因为需要向下兼容C语言的结果而导致的,但是,如果你想使用C++进行纯粹的OOP编程,那么你必须创建自己的对象树来解决问题域,也就是创建自己的类库来解决问题。这样做的结果是,C++程序员需要花大量的时间来设计自己的接口,这样有好处吗?有,如果问题域需要大量使用C来解决问题,那么使用C++是非常有价值的!但是,如果没有C的问题呢,那么如果你选择Java作来OOP的开发工具会更有效。使用单继承之后,所有对象都有一个相同的根基,你可以知道每个对象的都有的基本操作,所有对象可以很容易的在堆中创建,并且参数的传递变得非常简单。同时,单继承使用得实现“垃圾回收器”比较容易,这也是Java对C++的一个重要的改进技术。最后,单继承也对系统级别的操作,比如异常处理是非常有用的,以及可以让我们编程更具有灵活性,也就是Java是健壮语言的原因!

image-20210322165706422.png

容器

一般来说,你不会知道有多少对象可以解决一个特定的问题,或者说这些对象会使用多久。你也不知道这些对象是怎样被存贮的。那么你怎样才有知道创建这些对象时需要多少空间,而不是到运行才知道?

在面向对象的设计中这样解决问题的:创建一个新的对象,让新的对象使用另外的对象的引用来解决特定的问题。当然,我们也可以使用数组来解决相同的问题,这是大多数语言都这样做事的方式。但是,对于该这样的对象来说,一般被叫做一个“容器(container)”(也叫做集合collection, 但是Java使用”container”术语来表示),这样的结果,就是随时你可以向它添加需要的东西。所以,我们可不用知道这个容器能够装多少东西—我们只需要创造这样的容器,然后让它处理细节即可,也就是说对象是容器!

幸运的是,一个好的OOP语言会有一组容器,这些容器分别放在相应的包中。以C++语言中表现为标准的C++库(Standard C++ Library)和标准模板库(Standard Template Library--STL)。在Java中的标准库也有大量的容器,比如List类(顺序排列)、Map类(关联数据,使用一个对象引用另外一个对象)和Set类(只保存每个对象的类型),以及queue, tree, stack等工具类。

从设计的角度上讲,我们就是需要设计一个可以被操纵的容器来解决现实问题域。如果单继承可以满足这种要求,那么就没有理由选择别的方式来完成。

参数化类型(泛型)

在Java SE5之前,在Java中容器只有一个类型:Object. 单继承的,也就是说每个对象都Object类型,所以一个容器使用Object时表示所可以装载所有的东西,这样做让容器非常容易重用。为解决类型向下转型的问题,Java使用了参数化机制技术来解决这个问题。所谓参数化类型就是一个类,该类在编译时可以自动绑定到指定的类型。比如,我们使用一个参数化容器之后,那么编译器可以知道Shape不只是一个Shape,它可能是一个Cycle等的类型。比如,ArrayList = shapes new ArrayList();

image-20210322165906472.png

万物皆对象

image-20210322170001906.png

虽然Java是基于C++的语言,但是Java是一个更为纯粹的面向对象的语言。

使用引用操作对象

C++和Java都是混合语言,但是Java的设计者认为混合没有C++重要。所谓混合语言就是允许多种编程风格来书写代码,C++因为向后兼容C语言是一种混合语言,因此C++是C语言的超集,它包括了许多OOP中不应用有的特点,因此,C++是一种非常复杂的语言。

而Java语言是默认我们就是只做OOP编程的,所以在我们的头脑中应该是面向对象世界概念。这样做的结果是,会让我们学习OOP语言的变得简单。

每种编程语言都有自己的处理内存中元素的方式。但是就Java而言非常简单,所有对象都使用一种单一的、固定的格式来表示。因此,如果我们把任何事情都看成对象,那么我们所操纵的标识实际上就是一个对象的“引用”。比如,你想像一下一台电视和一个远程遥控器。我们手里拿的就是电视的引用—它连接到电视,当有人说“换频道”或者“声明关小一点”时,我们操纵的是遥控器(电视的引用,因它不是电视本身)。所以

​ String s;

表示只是创建了一个引用,而不是一个对象。如果需要实际的对象,那么需要赋值给这个引用

​ s = “遥控器”;

或者 s = new String(“摇控器”);

image-20210322170158605.png

对象被存贮的地方

在计算机中有五种不同的地方可以存放数据:

  1. 寄存器(Register)—它的存取速度最快,因为它在处理器内部位置。C和C++允许你使用它们。
  2. 栈(The stack)—它存在于random-access memory (RAM)蕊片中。处理器使用栈指针来使用它。它的存取速度非常快,紧次于寄存的速度。作为Java程序员必须知道,Java程序必须在栈中运行,这种限制导致编程程序的灵活性被限制,所以对于Java来说,我们把对象引用存放在栈中而对象本身不存放在栈中!!
  3. 堆(The heap)—它总体上说是一个内存池(也在RAM蕊片中),它是Java所有对象被存放的地方。使用堆的好处是Java编译器不需要知道对象的大小,这样给编译带来了灵活性。当我们需要一个对象时,我们只需要使用Java的关键字new来创建它,当代码执行时JVM会在堆中分配一个存贮空间给该对象。当然灵活性的代价就是:它比栈存贮对象花费的时间多。
  4. 固定存贮(Constant storage)—固定值一般是直接在代码放值,这样做的结果不安全,并且它们不会被修改。比如,嵌入式系统中的ROM放值。
  5. 非RAM存贮(Non-RAM storage)—如果数据完全在一个程序之外,那么表示它在程序不运行时也存在,也就是可以不被程序所控制。比如,流对象和持久对象(比如使用JDBC和Hibernate提供与数据库打交道的对象)。

特例:原始数据类型

image-20210322170505377.png

有数字类型都是有符号的;boolean类型没有大小,因为它被定义为两个字面量true和false。

对于高精确度的数字,在Java中使用BigInteger和BigDecimal类来完成计算。 BigInteger支持任意精度的整形,也就是说它可以表示任何整数值,而不会在运算时丢失精度。同样,BigDecimal可以实现任意固定小数的计算。

Java数组

在Java数组中,Java保证数据被实例化,并且不可以产生越界访问数组动作。当我们创建一个对象数组时,你实际上创建的是它们的引用的数组,并且这些引用自动被初始化为:null—当Java看见null时它表示该引用不指一个对象。但是,当我们需要使用一个引用时,我们必须给这个引用赋值,因此,如果在使用时引用的值还是null,那么报运行时错,从而,Java可以解决C和C++中的数组缺陷问题。

对于原始数据类型的数组,编程器会给数组分配零字节的空间给这个数组对象。

class:创建新数据类型

如果万物皆对象,那么是什么东西决定了这些对象的长像和行为呢?回答就是class!该关键字的意思是:“我要告诉你这种对象属性什么种类的。”语法如下:

class ATypeName{     

  //Class body goes here

}

当我们定义一个class(类)时,我们可以在类放两种类型的元素:field(有时候被叫做成员变量)和method(有时候叫做成员功能)。成员变量可以任意引用类型或者原始数据类型。如果成员变量是一个引用类型,那么在使用之前必须初始化它(使用new关键来实现)。每个对象都有自己的成员变量的存贮空间;一般成员变量不会被共享出来。比如:

class DataOnly{

  int i;

  double d;

  boolean b;

}

下面原始数据类型的默认值:

image-20210322170910316.png

综合演示原始数据类型使用语法

import static java.lang.System.*;
import java.util.*;
/**
	功能:书写一个类用来演示foreach的使用方式。
	作者:技术大黍
	*/
public class ForEachDemo{
	
	public ForEachDemo(){
		showFloat();
		new ListCharacters();//演示字母foreach
	}
	
	private void showFloat(){
		Random rand = new Random(47);
		float[] f =  new float[10];
		int[] integer = new int[10];
		
		for(int i = 0; i < 10; i++){
			f[i] = rand.nextFloat();
			integer[i] = rand.nextInt();
		}
		
		for(float x : f){
			out.println(x);
		}
		
		for(int x : integer){
			out.println(x);
		}
	}
	
	public static void main(String[] args){
		new ForEachDemo();
	}
}

/**
	功能:单独使用一个类用来完成判断ASCII码值的功能
	*/
class ListCharacters {
	
	public ListCharacters() {
		for(char c = 0; c < 128; c++){//判断128个ASCII码
			//使用Character包装类判断字母
			if(Character.isLowerCase(c)){
				out.println("值: " + (int)c +
				" 字母: " + c);
			}
		}
	}
}

I/O流对象

创建一个良好的输入/输出(I/O)系统对于语言的设计者来说是非常重要的。因为,输入输出可能包括:文档、控制台、网络连接等的交互情况,但是你需要以各种形式来与这些资源说话—顺序的、随机的、缓存的、二进制的、字符的、一次一行的、一个单词一个单词的等等方式与文件资源打交道。

File类从字面里理解是文件,但是实际它不是,它实际上是“FilePath”的意思,它表示特定文件的名称,或者在一个目录中的一组文件的意思。如果是一组文件,那么你可以使用list()方法,它可以返回一个String的对象数组。

如果我们需要完成一个目录列表的功能,那么File对象可以有两种被使用的方式。

I/O流演示代码

import static java.lang.System.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;

/**
	功能:书写一个类用来演示file类的使用
	作者:技术大黍
	备注:
		演示时使用正则表达式:S.*\.java
	*/
public class DirList{
	//声明一个成员变量path
	private File path;
	//声明一个成员变量list用来保存文件列表内容
	private String[] list;
	
	public DirList(String[] args){
		path = new File(".");
		//如果命令行中参数为0
		if (args.length == 0){
			//那么列出当前目录表
			list = path.list();
			//显示出来
			for(String x : list){
				out.println(x);
			}
		}else{
			//否则根据参数来列
			list = path.list(new DirFilter(args[0]));
			//使用Arrays对象把list排序
			Arrays.sort(list,String.CASE_INSENSITIVE_ORDER);
			//显示出来
			for(String x : list){
				out.println(x);
			}
		}
	}
}

class DirFilter implements FilenameFilter{
	//声明一个正则表达式成员变量,用来过滤输入的命令
	private Pattern pattern;
	
	public DirFilter(String regex){
		pattern = Pattern.compile(regex);
	}
	/**
		根据输入的命令来判断是否有文件存在
		*/
	public boolean accept(File dir, String name){
		return pattern.matcher(name).matches();
	}
}

文件类

DirFilter类的作用是提供一个accept()方法给File类的list方法说明该文件或目录是否存在—即是否应该放到list中去,也就是list方法可以“回调”accept()方法来决定是否放到列表中去—这是一种策略设计模式的应用。而在accept方法中使用正则表达式的matcher对象,由它来描述需要什么样了文件或者目录。

File类的应用主要是实现目录工具类,它们列出当前的目录路径和文件内容。代码参见下面个演示类

演示类-PathPrint

import static java.lang.System.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;


/**
	功能:书写一个迭代目录之后的树类
	作者:技术大黍
	*/
public class PathPrint{
	public static String pathFormat(Collection<?> collection){
		//如果没有元素
		if(collection.size() == 0){
			//那么返回[]
			return "[]";
		}
		//否则使用都要显示出来
		StringBuilder message = new StringBuilder("[");
		for(Object item : collection){
			//如果不大小不等于1
			if(collection.size() == 1){
				message.append("\n"); //换行
			}
			//否则取得内容
			message.append(item + "\n");
		}
		message.append("]");
		
		return message.toString();
	}
}

演示类-TreeInformation

import static java.lang.System.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;


/**
	功能:书写一个迭代目录之后的树类
	作者:技术大黍
	备注:该类是精典的迭代模式代码,关键思路就是把File的引用放集合中去,
		而集合中存在放File的引用。
	*/

public class TreeInformation implements Iterable<File>{
	public List<File> files = new ArrayList<File>();
	public List<File> dirs = new ArrayList<File>();
	
	//列出当前目录结构
	public Iterator<File> iterator(){
		return files.iterator();
	}
	/*把成员变量在递归算法中传递*/
	 void addAll(TreeInformation other){
		files.addAll(other.files);
		dirs.addAll(other.dirs);
	}
	
	/**
		实现对树的迭代动作
		*/
	public  TreeInformation walk(String start, String regex){
		return recurseDirs(new File(start), regex);
	}
	
	public  TreeInformation walk(File start, String regex){
		return recurseDirs(start, regex);
	}
	
	public  TreeInformation walk(File start){
		return recurseDirs(start, ".*");
	}
	
	public  TreeInformation walk(String start){
		return recurseDirs(new File(start), ".*");
	}
	
	/*使用递归来实现*/
	 TreeInformation recurseDirs(File startDir, String regex){
	 	out.println("startDir is " + startDir + ", regex is " + regex);
		TreeInformation result = new TreeInformation();
		
		for(File item : startDir.listFiles()){
			if(item.isDirectory()){ //如果是目录
				//那么添加到列表中去
				result.dirs.add(item);
				//然后递归添加
				result.addAll(recurseDirs(item,regex));
			}else{
				//否则如果是文件,那么需要根据正则表达式来判断
				if(item.getName().matches(regex)){
					//把文件添加到列表中去
					result.files.add(item);
				}
			}
		}
		return result;
	}
	
	public String toString(){
		return "目录:" + PathPrint.pathFormat(dirs)
			+ "\n\n文件:" + PathPrint.pathFormat(files);
	}
}

File类--检查并创建目录

File类除可以表示一个目录或者文件是否存在以外,还可以使用File对象来创建一个新目录。代码参见MakeDirectories类

import static java.lang.System.*;
import java.io.*;

/**
	功能:书写一个处理文档的工具类
	作者:技术大黍
	备注:
		注意我们使用策略模式来处理文档对象
	*/
	
public class MakeDirectories{
	private static void usage(){
		StringBuilder message = new StringBuilder();
		message.append("使用格式: MakeDirectories 路径1 ...\n");
		message.append("创建每个路径\n");
		message.append("使用格式: MakeDirectories -d path1 ...\n");
		message.append("删除每个路径\n");
		message.append("使用格式: MakeDirectories -r path1 path2\n");
		message.append("把path1更名path2\n");
		err.println(message.toString());
		exit(1); //关闭系统
	}
	
	private static void fileData(File file){
		StringBuilder message = new StringBuilder();
		message.append("绝对路径:" + file.getAbsolutePath() + "\n");
		message.append("只读:" + file.canRead() + "\n");
		message.append("可写:" + file.canWrite() + "\n");
		message.append("getName:" + file.getName() + "\n");
		message.append("getParent: " + file.getParent() + "\n");
		message.append("getPath: " + file.getPath() + "\n");
		message.append("长度:" + file.length() + "\n");
		message.append("最后一次修改时间:" + file.lastModified());
		out.println(message.toString());
		//判断当前的路径是什么
		if(file.isFile()){
			out.println("当前路径是一个文件");
		}else{
			out.println("当前路径是一个目录");
		}
	}
	
	public MakeDirectories(String[] args){
		if(args.length < 1){//如果参数列表小于1
			//那么打印一句话说明怎样使用该类
			usage();
		}
		//否则
		if(args[0].equals("-r")){//如果参数是-r
			if(args.length != 3){
				usage();
			}
			File old = new File(args[1]);
			File rename = new File(args[2]);
			old.renameTo(rename);
			fileData(old);
			fileData(rename);
			return; //退出主线程
		}
		//下面处理删除目录的事务
		int count = 0;
		boolean del = false;
		if(args[0].equals("-d")){
			count++;
			del = true;
		}
		count--;
		while(++count < args.length){
			File file = new File(args[count]);
			if(file.exists()){
				out.println(file + "已经存在了");
				if(del){
					out.println("删除..." + file);
					file.delete();
				}
			}else{//否则不存在
				if(!del){
					file.mkdirs();
					out.println(file + "已创建!!");
				}
			}
			fileData(file);
		}
	}
}

输入和输出

编程语言的I/O库一般用来表示一种流的抽象,该流表示数据源像一个水槽的东西,它可以产生和接收数据片断,而流隐藏了实际I/O设备的实现细节。

Java的I/O库被分为输入和输出两大部分,通过继承树我们可以知道,对于输入流都是使用InputStream或者Reader类作为基类,然后呼叫read()方法来读取单个字节或者字节数组;相对应的是输出流都OutputStream或者Write类提供write()方法来写一个字节或者字节数组。然而,实际开发中,我们一般不使用这些方法;我们可使用其它的接口方法来表示,主要使用Decorator模式来完成。

InputStream流的类型

InputStream表示的类型是根据不同的资源来定的:

  • 字节数组
  • 一个String对象
  • 一个文件
  • 一个“管道”,有点儿像物理的管理:你可把东西从一端放进去,然后从另一端拿出来 其它的流对象序列,然后把它集中成一个流对象
  • 其它的资源,比如Internet连接得到的流对象(参见”Thinking in Enterprise Java”一书)

其中FilterInputStream也是一种InputStream对象,它提供”decorator”的基类。

image-20210322172847695.png

image-20210322172855954.png

OutputStream流的类型

image-20210322172926030.png

image-20210322172932155.png

添加属性和有用的接口

在Java I/O库中FilterInputStream和FilterOutputStream分别是控制InputStream和OutputStream的”decorator”接口对象,而后者是实现decorator模式的关键(它提供所有被decorated的对象的通用接口)。

下面我们来看怎样使用FilterInputStream来从输入流对象中读取信息。

image-20210322173044281.png

FilterInputStream类完成两个非常重要的事情:第一个是DataInputStream允许我们把各种原始数据类型读成String对象(所有的方法都是以“read”开头,比如readByte(), readFloat()等),相应的使用DataOutputStream对象来完成把这些数据类型放到另外一个流对象中去。

其它的类型就像InputStream的内部行为:不管是否为缓存中操作流对象,最后两个类支持编译时构建的动作,所它们不可以使用在泛型编程中。

image-20210323084551512.png

PrintStream的原意是打印所有数据类型,包括原始数据类型和String类型,它使用print()方法和println()方法输出数据。该类支持checkError()方法,但是不支持国际化与跨平台操作,该问题在PrintWriter类中得到解决。

BufferedOutputStream是一种输出流的缓存机制。

image-20210323084725538.png

Reader和Writer

InputStream和OutputStream类是面向字节的I/O流对象,而Reader和Writer类提供了面向字符的、并且是支持Unicode码的I/O流对象。使用Reader与Writer的最重要原因是国际化的需求。原旧的I/O流对象只支持8位流对象,而不支持处理16位Unicode码的流对象的处理,另外Reader和Writer的操作速度比旧的流对象快。

注意:RandomwAccessFile不属于InputStream和OutputStream继承体系,但是它可以在一个文件中向前向后移动。它使用方法getFilePointer()来定位处理于文件的当前位置,使用seek()方法把当前指针移到到新的位置。

RandomAccessFileDemo演示类

import static java.lang.System.*;
import java.io.*;

/**
	功能:书写一个处理文档的工具类--使用RandomAccessFile类
	作者:技术大黍
	备注:
		使用RandomAccessFile对象,可以实现对文件的任意位置进行读/写操作。
	*/
public class RandomAccessFileDemo{
	
	public RandomAccessFileDemo(String fileName, String priority){
		try{
			RandomAccessFile file = new RandomAccessFile(fileName,priority);
			//一行一行读取文件内容
			for(int i = 0; i <= 4; i++){
				out.println(file.readLine());
			}
			//指定一个当前的文件指针
			long current = file.getFilePointer();
			//根据文件指针来移动
			file.seek(current + 2);
			//根据移动后的位置添加内容
			file.write("老九学堂--技术大黍".getBytes());
			//然后再显示
			for(int i = 0; i <= 5; i++){
				out.println(file.readLine());
			}
			current = file.getFilePointer();
			file.seek(current + 2);
			file.write("27.".getBytes());
			file.close();
		}catch(Exception e){
			out.println(e.getMessage());
		}
	}
}

内存中的格式化输入演示代码

import static java.lang.System.*;
import java.io.*;

/**
	功能:书写一个处理文档的工具类,从内存中读取内容
	作者:技术大黍
	备注:
		主要是unicode字符流对象来实现
	*/
public class MemoryInput{
	
	public MemoryInput(String filename){
		try{
			StringReader input = new StringReader(BufferedInputFileDemo.readFile(filename));
			//使用StringReader从缓存中读Unicode码
			int contant = 0;
			//然后使用unicode码显示在屏幕上
			while((contant = input.read()) != -1){
				out.print((char)contant);
			}
		}catch(Exception e){
			out.println(e.getMessage());
		}
	}
}

一般的文件输出我们使用FileWriter对象 来完成,实际中我们使用PrintWriter对象提供格式化的输出。

文件输出演示

import static java.lang.System.*;
import java.io.*;
import com.jb.arklis.io.input.*;

/**
	功能:书写一个输出文档的类
	作者:技术大黍
	*/
public class BasicFileOutputDemo{
	/**
		out为指定的输出文件名,in为输入的文件名称
		*/
	public static void outputFile(String outFile, String inFile){
		try{
			//读入文件
			BufferedReader input = new BufferedReader(
				new StringReader(BufferedInputFileDemo.readFile(inFile)));
			//输出文件
			PrintWriter output = new PrintWriter(
				new BufferedWriter(new FileWriter(outFile)));
			int count = 1;
			String temp;
			//逐行输出,输出时加行号
			while((temp = input.readLine()) != null){
				output.println(count++ + ": " + temp);
			}
			output.close();
			//显示输入的信息
			out.println(BufferedInputFileDemo.readFile(inFile));
		}catch(Exception e){
			out.println(e.getMessage());
		}
	}
}

除了使用PrintWriter进行格式输出以外,我们还可以使用DataOutputStream和DataInputStream对象来进行数据的保存,以及恢复的动作。

数据对象输入输出

import static java.lang.System.*;
import java.io.*;
import com.jb.arklis.io.input.*;

/**
	功能:书写一个保存数据的类
	作者:技术大黍
	备注:使用DataOutputStream来实现
	*/

public class StoringAndRecoveringData{
	
	public static void storeData(String outFile){
		try{
			DataOutputStream output = new DataOutputStream(
				new BufferedOutputStream(
				new FileOutputStream(outFile)));
			output.writeDouble(3.14159);//保存浮点数
			output.writeUTF("这是一个PI值"); //保存字符串
			output.writeDouble(1.41413);
			output.writeUTF("这是2的平方值");
			output.close();
			//显示输结果
			DataInputStream input = new DataInputStream(
				new BufferedInputStream(
				new FileInputStream(outFile)));
			//使用标准输出设置显示
			out.println(input.readDouble());
			out.println(input.readUTF());
			out.println(input.readDouble());
			out.println(input.readUTF());
		}catch(Exception e){
			out.println(e.getMessage());
		}
	}
}

内存中影射文件

import static java.lang.System.*;
import java.nio.channels.*;
import java.nio.*;
import java.io.*;
import javax.swing.*;
import static Print.*;

/**
	功能:书写一个测试类,用来测试传统的流与新通道对象的速度区别
	作者:技术大黍
	备注:
	*/
public class MappedIOTest{
	public static int numOfInts = 4000000;
	public static int numOfUbuffInts = 200000;
	//定义一个抽象类,用来测试I/O
	public abstract static class Tester {
		private String name;
		public Tester(String name) { 
			this.name = name; 
		}
		public void runTest() {
			System.out.print(name + ": ");
			try {
				long start = System.nanoTime();
				test();
				double duration = System.nanoTime() - start;
				System.out.format("%.2f\n", duration/1.0e9);
			} catch(IOException e) {
				throw new RuntimeException(e);
			}
		}
		public abstract void test() throws IOException;
	}
	//定义一个Tester数组
	public static Tester[] tests = {
		new Tester("Stream Write(流对象写temp.tmp文件)") {
			public void test() throws IOException {
				DataOutputStream dos = new DataOutputStream(
				new BufferedOutputStream(
				new FileOutputStream(new File("temp.tmp"))));
				for(int i = 0; i < numOfInts; i++)
					dos.writeInt(i);
				dos.close();
			}
		},
		new Tester("Mapped Write(通道对象写temp.tmp文件)") {
			public void test() throws IOException {
				FileChannel fc =
				new RandomAccessFile("temp.tmp", "rw")
				.getChannel();
				IntBuffer ib = fc.map(
				FileChannel.MapMode.READ_WRITE, 0, fc.size())
				.asIntBuffer();
				for(int i = 0; i < numOfInts; i++)
					ib.put(i);
				fc.close();
			}
		},
		new Tester("Stream Read(流对象读temp.tmp文件)") {
			public void test() throws IOException {
				DataInputStream dis = new DataInputStream(
				new BufferedInputStream(
				new FileInputStream("temp.tmp")));
				for(int i = 0; i < numOfInts; i++)
					dis.readInt();
				dis.close();
			}
		},
		new Tester("Mapped Read(通道对象读temp.tmp文件)") {
			public void test() throws IOException {
				FileChannel fc = new FileInputStream(
				new File("temp.tmp")).getChannel();
				IntBuffer ib = fc.map(
				FileChannel.MapMode.READ_ONLY, 0, fc.size())
				.asIntBuffer();
				while(ib.hasRemaining())
					ib.get();
				fc.close();
			}
		},
		new Tester("Stream Read/Write(流对象读/写temp.tmp文件)") {
			public void test() throws IOException {
				RandomAccessFile raf = new RandomAccessFile(
				new File("temp.tmp"), "rw");
				raf.writeInt(1);
				for(int i = 0; i < numOfUbuffInts; i++) {
					raf.seek(raf.length() - 4);
					raf.writeInt(raf.readInt());
				}
				raf.close();
			}
		},
		new Tester("Mapped Read/Write(通道对象读/写temp.tmp文件)") {
			public void test() throws IOException {
				FileChannel fc = new RandomAccessFile(
				new File("temp.tmp"), "rw").getChannel();
				IntBuffer ib = fc.map(
				FileChannel.MapMode.READ_WRITE, 0, fc.size())
				.asIntBuffer();
				ib.put(0);
				for(int i = 1; i < numOfUbuffInts; i++)
					ib.put(ib.get(i - 1));
				fc.close();
			}
		}
	};
}

文件锁可以实现同步访问一个文件作为共享资源。如果两个线程来自不同的JVM时,或者一个是Java线程与OS的本地线程时,文件锁可以实现让OS进程看到该锁,因为Java的文件锁直接影射本地操作系统的锁机制!

文件锁演示

import static java.lang.System.*;
import java.nio.*;
import java.nio.channels.*;
import java.io.*;

/**
	功能:书写一个类用来演示文件锁的使用
	作者:技术大黍
	备注:
		注意这里使用内存影射文件与文件锁的功能
	*/
public class LockingMappedFiles{
	static final int LENGTH = 0x4FFFF; //128MB 0x8FFFFFF
	static FileChannel channel;
	
	public static void showFileLock()throws IOException{
		channel = new RandomAccessFile("testfilelock.dat","rw").getChannel();
		//内存影射文件
		MappedByteBuffer output = channel.map(FileChannel.MapMode.READ_WRITE, 0, LENGTH);
		//使用内存影射文件对通道进行写操作
		for(int i = 0; i < LENGTH; i++){
			output.put((byte)'x');
		}
		//对通道进行锁定
		new LockAndModify(output, 0, 0 + LENGTH / 3);
		new LockAndModify(output, LENGTH / 2, LENGTH / 2 + LENGTH / 4);
	}
	
	//定义一个私有表态类,用来处理锁定义文件的动作
	private static class LockAndModify extends Thread{
		private ByteBuffer buffer;
		private int start, end;
		
		//在构造方法初始化成员变量
		LockAndModify(ByteBuffer buffer, int start, int end){
			this.start = start;
			this.end = end;
			buffer.limit(end);//指定缓存上限
			buffer.position(start);//指定缓存起始位置
			this.buffer = buffer.slice();
			start(); //起动线程
		}
		
		//重写run()方法
		public void run(){
			try{
				//执行锁定动作
				FileLock lock = channel.lock(start, end, false);
				//显示锁定结果
				out.println("Locked:(锁定) " + start + " to(到) " + end);
				//执行修改动作
				while(buffer.position() < buffer.limit() - 1){
					buffer.put((byte)(buffer.get() + 1));
				}
				//释放锁
				lock.release();
				out.println("Released:(释放) " + start + " to(到) " + end);
			}catch(Exception e){
				out.println(e.getMessage());
			}
		}
	}
}

要实现序列化一个对象,我们只需要创建一些OutputStream对象和ObjectOutpuStream对象,然后我们呼叫writeObject()方法把被序列化的对象发送给OutputStream对象即可;那么读取序列化的动作相反,我们使用InputStream对象和ObjectIntputStream对象来实现。

对象序列化演示

import static java.lang.System.*;
import java.io.*;
import java.util.*;
import static Print.*;

/**
	功能:书写一个类用来演示对象序列化的使用
	作者:技术大黍
	备注:
		该示例使用对象链表、原始数据类型来说明序列化的效果。
	*/
public class Worm implements Serializable{
	private static Random rand = new Random(47);
	
	private Data[] datas = {
		new Data(rand.nextInt(10)),
		new Data(rand.nextInt(10)),
		new Data(rand.nextInt(10))
	};
		
	private Worm next;
	
	private char c;
	
	/*
		使用引用来构造一个链表
		*/
	public Worm(int i, char x){
		print("Worm的构造方法:" + i);
		c = x;
		if(--i > 0){
			next = new Worm(i,(char)(x + 1));
		}
	}
	
	public Worm(){
		print("默认的构造方法");
	}
	
	public String toString(){
		StringBuilder result = new StringBuilder(":");
		result.append(c);
		result.append("(");
		for(Data x : datas){
			result.append(x);
		}
		result.append(")");
		
		if(next != null){
			result.append(next);
		}
		
		return result.toString();
	}
	
	public void showSerializable()throws Exception{
		Worm worm = new Worm(6, 'a');
		print("worm = " + worm);
		//序列化对象worm
		ObjectOutputStream output = new ObjectOutputStream(
			new FileOutputStream("worm.out"));
		output.writeObject("Worm已被保存(序列化)了\n");
		output.writeObject(worm);
		output.close();
		//读取被序列化的对象
		ObjectInputStream input = new ObjectInputStream(
			new FileInputStream("worm.out"));
		String temp = (String)input.readObject();
		//重新通过序列化构造对象
		Worm wormTwo = (Worm)input.readObject();
		print(temp + "Worm Two = " + wormTwo);
		ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
		ObjectOutputStream outTwo = new ObjectOutputStream(byteOutput);
		outTwo.writeObject("Worm被序列化\n");
		outTwo.writeObject(wormTwo);
		outTwo.flush();
		//再读--通过ByteArrayInputStream和outpuStream
		ObjectInputStream inputTwo = new ObjectInputStream(
			new ByteArrayInputStream(byteOutput.toByteArray()));
		temp = (String)inputTwo.readObject();
		Worm wormThree = (Worm)inputTwo.readObject();
		print(temp + "Worm Three = " + wormThree);
		print("wormThree.getClass() is " + wormThree.getClass());
	}
}

class Data implements Serializable{
	private int n;
	public Data(int n){
		this.n = n;
	}
	
	public String toString(){
		return Integer.toString(n);
	}
}

//辅助类
public class Print {
  // Print with a newline:
  public static void print(Object obj) {
    System.out.println(obj);
  }
  // Print a newline by itself:
  public static void print() {
    System.out.println();
  }
  // Print with no line break:
  public static void printnb(Object obj) {
    System.out.print(obj);
  }
  // The new Java SE5 printf() (from C):
  public static PrintStream printf(String format, Object... args) {
    return System.out.printf(format, args);
  }
}

正则表达式

Regular expressions (正则表达式)原本是Unix操作系统的一个标准工具包,它属于Unix的一部分。在Java中String, StringBuffer和StringTokenizer类可以方便的使用正则表达式。因为正则表达式是非常强大的文本处理工具。

image-20210322155142258.png

基本概念

从广泛的术语上说,一个正则表达式是一种描述字符串样子的方式,所以你可以说“如果字符串中有这些东西,那么它会匹配我想找的东西”。比如,一个数字可能带一个“减号”,那么你可以这样描述:

​ -?

来表示一个或者多个整数的样子。在正则表达式中,一个数字是这样被描述的’\d’. 如果在其它语言中使用’\\’,那么可能表示插入一个’\’字符,但是在Java中则表示不一样的意思,而如果插入’\’,那么使用’\\\\’表达式。就以上例子,我们可以加一个表达式

​ -?\\d+

表示可能有一个负号,在它的右边至少是一个数字。

通过String类可以非常简单的使用正则表达式,呼叫String类的matches方法即可。

System.out.println(“-1234”.matches(“-?\\d+”));
System.out.println(“5678”.matches(“-?\\d+”));
System.out.println(“+911”.matches(“-?\\d+”));
System.out.println(“+911”.matches(“(-|\\+)?\\d+”));

第三个表达式不为真,因为’+’虽然是合法的,但是不是正则表达式指定的格式,所以它匹配结果是false值。在正则表达式中,我们使用’()’进行分组,使用’|’表示OR的运算关系。所以

​ (-|\+)?

表示了可以’-’或者’+’或者没有(因为’?’作为非贪吃表达式组合存在)。

String类还有一个替换方法replace()可以非常方便使用正则表达式来替换原字符串的内容。

image-20210323093731061.png

image-20210323093739182.png

image-20210323093758445.png

image-20210323093808079.png

验证”Rudolph”字符串可以使用以下几种匹配策略:

“Rudolph”, “[rR]udolph”, “[rR][aeiou][a-z]ol.”,“R.”等等样式都可以实现。

数量修改

一个数量修改符描述了表达式读取的文本长度:

  1. Greedy(贪吃)—greedy表示是数量是贪吃的,除非被描述符被修改了。一个贪吃的表达式的目的是:尽可能多的匹配表达式。它的后果是会影响右边相邻的表达式的表示。
  2. Reluctant(非贪吃)—使用’?’表示,该表达式以尽可能少的方式匹配表达式,也就叫lazy匹配、最小匹配、非贪吃匹配或者ungreedy匹配。
  3. Possessive(所有)—只在Java语句中使用, 它比较高级,所以可能会误用。它用来阻止正则表达式不执行的行为,也可让正则表达式更有效的执行。

image-20210323094025963.png

比如: abc+ 以上表达式似乎是想表示匹配以’abc’开头的多个字符串,但是实际上,当你输入’abcabcabc’时,它会匹配三次。而事实上,我们需要“匹配以ab开头,然后右边至少一个英文字符”。如果是这样,那么你必须这样写表达式: (abc)+ 实事上,正则表达式会经常愚弄我们开发人员,它是一个与Java十字交叉的语言!

Pattern和Matcher类

总体来讲,如果不想受String的限制,那么可以使用正则表达式对象来处理表达式。正则表达式在Unix/Linux系统中必须使用绰号来标识。 Pattern对象表示一个被编译版本的正则表达式,然后通过该对象的matcher()方法和输入字符产生一个Matcher对象。其中,matches()方法用来检查整个输入字符串是否匹配;split()方法产生一个字符串数组;find方法和lookingAt()方法用来访问匹配的结果。

分组

分组在正则表达式中使用圆括号’()’来完成,并且可以通过组号来重新被呼叫。组号0表示匹配整个表达式,组号1表示最外面的括号表达式,比如: A(B(C))D Group 0表示ABCD, group 1表示BC, group 2表示C等。Matcher类的groupCount()方法返回组的个数,但是不包括Group 0在内。group()方法返回find方法匹配之后组号为0的字符串;group(int i)方法返回指定组号的字符串,如果失败返回null值;int start(int group)返回组的开始下标值;int end (int group)返回组的结束下标值。

start()和end()

Matcher类的start()方法返回匹配的开始下标值,end方法返回匹配的结束下标值。另外,如果两个方法如果匹配不成功时会抛出IllegalStateException异常。

注意find()方法会对输入字符进行任意位置的定位,但是lookingAt()和matches()只有在表达式输入每次开始匹配时存在。其中,matches()方法只有在整个表达式都匹配的情况下才能返回成功,lookingAt()方法只有在输入字符串第一部分匹配时返回成功。

Pattern flags(表达式标记)

类Pattern在呼叫compile()方法时可以接收匹配行为标记如下:

Pattern.compile(String regex, int flag); 标记是一个常量如下:

image-20210323094352117.png

image-20210323094424418.png

注意:”^java”中的’^’不在’[ ]’中时表示不需要重复匹配的字符串。所以它等于”^(java)”、”^j(ava)”。

split()和replace()方法

split( ) divides an input string into an array of String--这是Pattern类的split方法的效果。replace方法是非常有用的方法,它特别用来替换文本中的内容。其中Matcher类有如下的有用的方法:

  • replaceFirst(String replacement)—把参数用来替换第一个匹配的部分。
  • replaceAll(String replacement)—把参数替换所有匹配的部分。
  • appendReplacement(StringBuffer sbuf, String replacement)—把sbuf一个一个替换,而不是把第一个或者所有都替换。这是一个非常重要的方法,它允许你呼叫方法来执行替换处理动作(replaceFirst或replaceAll)。
  • appendTail(StringBuffer sbu, String replacement)—表示当至少有一个appendReplacement方法被调用之后被调用。

reset()方法

注意:replaceFirst()方法和replaceAll()方法只是字面量(literal),所以如果你想执行具体的替换动作,它们是没有帮助的。实际上,我们需要使用appendReplacement方法才能实现具体的替换动作。appendTail()方法一般是在执行了所有替换之后,然后呼叫该方法。

Matcher类的reset()方法可以作用于一个新的字符序列。

正则表达式和Java I/O

下面的例子是使用正则表达式来查找文件中的内容,它模拟Unix操作系统中的grep命令—JGrep.java有两个参数:一个是文件名,另外一个是正则表达式。输出的结果是有匹配字符串的行号,以及字样和开始位置编号。

扫描输入信息

Scanner类的构造方法可以任意的输入对象,包括File对象都可以作为构建方法的参数使用,以及InputStream和String等等类型。Java SE5的目的是让定义一个类有一个read()任何一切的方法。BufferedReader类没有这个效果。

所以,在JDK 1.5以后引用引用一个工具类Scanner类简化了读取的操作。

正则表达式综合演示

以上所讲内容,我们使用一个综合代码示来演示,希望能够帮助大家快速理解。

import static java.lang.System.*;
import java.util.regex.*;
import java.util.*;

/**
	功能:书写一个测试正则表达式的演示
	作者:技术大黍
	*/
public class ShowRegularExpression{
	//使用内部类对象来演示
	private class IntegerMath{
		public IntegerMath(){
			out.println("\"-1234\".matches(\"-?\\\\d+\") is " + "-1234".matches("-?\\d+"));
			out.println("\"5678\".matches(\"-?\\\\d+\") is " + "5678".matches("-?\\d+"));
			out.println("\"+911\".matches(\"-?\\\\d+\") is " + "+911".matches("-?\\d+"));
			out.println("\"+911\".matches(\"(-|\\\\+)?\\\\d+\") is " + "-1234".matches("(-|\\+)?\\d+"));
		}
	}
	
	private class Splitting{
		private String knights = "Then, when you have found the shrubbery, you must "
			+ "cut down the mightiest tree in the forest... with ... a herring!";
		
		public Splitting(){
			split(" "); //无正则表达式
			split("\\W+"); //非字符
			split("n\\W+"); //'n'右边跟非字符
		}
		
		public Splitting(String content){
			replace(content);
		}
		
		private void split(String regex){
			out.println(Arrays.toString(knights.split(regex)));
		}
		//测试String类的replace方法的使用
		private void replace(String content){
			out.println(knights);
			out.println(knights.replaceFirst("f\\w+",content)); //替换每一个匹配的字符
			out.println(knights.replaceAll("shrubbery|tree|herring", content)); //替换所有匹配的字符
		}
	}
	
	public ShowRegularExpression(){
		//new IntegerMath(); //演示String类的matches方法使用
		//new Splitting(); //测试拆分字符串
		//new Splitting("测试字符"); //测试replace方法
		//testMultiPattern(); //测试多个表达式的使用
		//new RegularTestUtil(); //测试正则表达式工具
		//new Finding(); //测试Matcher类的find方法的使用
		//new Groups(); //测试分组效果
		//new StartEnd().showStartAndEnd(); //演示matcher之后的start方法与end方法值
		//new ReFlags(); //测试Pattern的标记位
		//new SplitDemo(); //测试Patern的split方法
		//new TheReplacements(); //演示replace方法的使用
		//new Restting(); //测试Mathcer类的reset方法
		//new JGrep("\\S+","ShowIOStream.java");//演示正则表达式与文件的配合使用
		new ThreatAnalyzer(); //演示正则表达式与Scanner类的配合使用
	}
	
	private void testMultiPattern(){
		for(String pattern : new String[]{
			"Rudolph","[rR]udolph","[rR][aeiou][a-z]ol.*",
			"R.*","R.+",".*"}){
			//根据pattern数组来测试输入数据
			out.println("\"Rudolph\".matches(" + pattern + ") is " + "Rudolph".matches(pattern));
		}
	}
	
	public static void main(String[] args){
		new ShowRegularExpression();
	}
}

class RegularTestUtil{
	private String pattern;
	private String content;
	private Scanner scanner;
	
	public RegularTestUtil(){
		init(); //初始化成员变量
		test(); //执行测试动作
	}
	
	private void init(){
		scanner = new Scanner(System.in); //从键盘接收输入
		out.println("输入正则表达式:");
		//如果没有输入
		pattern = scanner.nextLine();
		if(pattern == null || pattern.trim().length() == 0){
			out.println("请正则表达式和需要测试的数据^_^");
			//那么结束程序
			exit(0);
		}
		//否则执行测试
	}
	
	private void test(){
		out.println("输入测试数据:");
		content = scanner.nextLine();
		//具体执行测试
		Pattern testPattern = Pattern.compile(pattern);
		Matcher matcher = testPattern.matcher(content);
		//如果找到匹配对象
		while(matcher.find()){
			//那么打印出来 
			Print.print("匹配 \"" + matcher.group() +"\" 位置:" 
				+ matcher.start() + "-" + (matcher.end() - 1));
		}
	}
}

class Finding{
	private Matcher matcher = Pattern.compile("\\w+").matcher("Evening is full of the linnet's wings");
	
	public Finding(){
		while(matcher.find()){
			Print.printnb(matcher.group() + " ");
		}
		Print.print();
		int i = 0; 
		//演示find与group方法之间的关系。
		while(matcher.find(i)){
			Print.printnb(matcher.group() + " ");
			i++;
		}
	}
}

class Groups{
	private final String poem = 
		"Twas brilling, and the slithy toves\n" +
		"Did gyre and gimble in the wabe.\n" +
		"All mimsy were the borogoves,\n" +
		"And the mome raths outgrabe.\n\n" +
		"Beware the Jubjub bird, and shu\n" +
		"The frumious Bandersnatch.";
	private Matcher matcher = Pattern.compile("(?m)(\\S+)\\s+((\\S+)\\s+(\\S+))$")
		.matcher(poem);
	
	public Groups(){
		while(matcher.find()){
			for(int i = 0; i <= matcher.groupCount(); i++){
				Print.printnb("[" + matcher.group(i) + "]");
			}
			Print.print();
		}
	}
}

class StartEnd{
	private String input = "As long as there is injustice, whenever a\n" +
		"Targathian baby cries out, wherever a distress\n" +
		"signal sounds among the stars ... We'll be there.\n" +
		"This fine ship, and this fine crew ... \n" +
		"Never give up! Never surrender!";
	
	private class Display{
		private boolean regexPrinted = false;
		private String regex;
		Display(String regex){
			this.regex = regex;
		}
		
		void display(String message){
			if(!regexPrinted){
				Print.print(regex);
				regexPrinted = true;
			}
			Print.print(message);
		}
	}
	
	public void examine(String content, String regex){
		Display display = new Display(regex);
		Pattern pattern = Pattern.compile(regex);
		Matcher matcher = pattern.matcher(content);
		//匹配查询
		while(matcher.find()){
			display.display("find() '" + matcher.group() + "' start = "
			 + matcher.start() + " end = " + matcher.end());
		}
		//如果正在找
		if(matcher.lookingAt()){
			display.display("lookingAt() start = " + matcher.start() +
				" end = " + matcher.end());
		}
		//如果匹配
		if(matcher.matches()){
			display.display("matches() start = " + matcher.start() +
				" end = " + matcher.end());
		}
	}
	
	void showStartAndEnd(){
		for(String in : input.split("\n")){
			Print.print("输入:" + in);
			for(String regex : new String[]{"\\w*ere\\w*",
				"\\w*ever","T\\w*","Never.*?!"}){
				examine(in, regex);
			}
		}
	}
}

class ReFlags{
	private Pattern pattern = Pattern.compile("^j(ava)",Pattern.CASE_INSENSITIVE|Pattern.MULTILINE);
	
	public ReFlags(){
		Matcher matcher = pattern.matcher("java has regex\nJava has regex\n" +
			"JAVA has pretty good regular expressions\n" + 
			"Regular expressions are in Java");
		while(matcher.find()){
			out.println(matcher.group());
		}
	}
}

class SplitDemo{
	private String input = "This!!unusual use!!of exclamation!!points";
	
	public SplitDemo(){
		out.println(Arrays.toString(Pattern.compile("!!").split(input)));
		out.println(Arrays.toString(Pattern.compile("!!").split(input,3)));
	}
}

class TheReplacements{
	//读取本资源文件
	private String temp = TextFile.read("src/com/jb/arklis/reg/TheReplacements.java");
	//匹配结束符,替换/*...*/开头的字符串
	private Matcher matcher = Pattern.compile("/\\*!(.*)!\\*/",Pattern.DOTALL).matcher(temp);
	
	public TheReplacements(){
		//如果有匹配字符
		if(matcher.find()){
			//那么取出第一个分组
			temp = matcher.group(1);
		}
		//把两个以空格替换了换成一个空格
		temp = temp.replaceAll(" {2,}", " ");
		//把每行开头换成非空格--这只是一个字面量,没有具体执行替换动作!!!
		temp = temp.replaceAll("(?m)^ +","");
		//把aeiou字符换成了VOWEL1字样--这只是一个字面量,没有具体执行替换动作!!!
		temp = temp.replaceFirst("[aeiou]","(VOWEL1)");
		
		StringBuffer message = new StringBuffer();
		Pattern pattern = Pattern.compile("[aeiou]");
		Matcher tempMatcher = pattern.matcher(temp);
		//处理找到的信息
		while(tempMatcher.find()){
			//实际执行替换的方法appendReplacement!!!把元音字母替换成大写
			tempMatcher.appendReplacement(message,tempMatcher.group().toUpperCase());
		}
		tempMatcher.appendTail(message);
		out.print(message);
	}
}

class Restting{
	private Matcher matcher = Pattern.compile("[frb][aui][gx]").matcher("fix the rug with bags");
	
	public Restting(){
		while(matcher.find()){
			out.print(matcher.group() + " ");
		}
		out.println();
		//新创建一行被匹配的字符串值
		matcher.reset("fix the rig with rags");
		while(matcher.find()){
			out.print(matcher.group() + " ");
		}
	}
}

class JGrep{
	private Pattern pattern;
	private Matcher matcher;
	
	public JGrep(String regex,String fileName){
		//初始化成员变量
		pattern = Pattern.compile(regex);
		//定义需要行号
		int index = 0;
		matcher = pattern.matcher("");
		//查找
		for(String line : new TextFile(fileName)){
			matcher.reset(line);
			while(matcher.find()){
				out.println("第" + ++index + "行: " + matcher.group()
					+ " 开始位置:" + matcher.start());
			}
		}
	}
}

保存对象

最简单的程序是只有一些固定的对象, 这些对象存在于该程序的生命周期中。因为不知道程序会有多大,所以我们需要一种容器来保存这些对象。

在Java的集合(容器)中,Map是一个关联数组,使用一个对象来表示另外一个对象,而Java容器类会自动调整该数组的大小。容器类是Java中的基本工具,它可以把程序变量非常强大。

image-20210322155142258.png

在Java中因为有Collection接口,所以我们使用“容器”来讨论集合对象。

下面我们使用代码来演示对象的保存。

import static java.lang.System.*;
import java.util.*;

/**
	功能:书写一个类用来演示泛型与非泛型编程
	作者:技术大黍
	*/
public class ShowGenericType{
	
	public ShowGenericType(){
		//使用非泛型
		//showWithOutGenerics();
		//使用泛型
		//showWithGenericType();
		//显示泛型的类型转换
		showGenericTypeUpcase();
	}
	
	//使用非泛型编程
	@SuppressWarnings("nuchecked")
	private void showWithOutGenerics(){
		List apples = new ArrayList();
		for(int i = 0; i < 3; i++){
			apples.add(new Apple());
		}
		//添加Orange
		apples.add(new Orange());
		//显示Apple
		for(int i = 0; i < apples.size(); i++){
			out.println("当前Apple数是:" + ((Apple)apples.get(i)).id());
		}
	}
	
	//下面是使用泛型编程
	private void showWithGenericType(){
		List<Apple> apples = new ArrayList<Apple>();
		for(int i = 0; i < 3; i++){
			apples.add(new Apple());
		}
		//添加Orange
		//apples.add(new Orange());
		//显示Apple
		for(int i = 0; i < apples.size(); i++){
			out.println("当前Apple数是:" + ((Apple)apples.get(i)).id());
		}
	}
	
	//说明泛型的类型转换
	private void showGenericTypeUpcase(){
		List<Apple> apples = new ArrayList<Apple>();
		apples.add(new GrannySmith());
		apples.add(new Gala());
		apples.add(new Fuji());
		apples.add(new Braeburn());
		//显示容器中的对象
		for(Apple x: apples){
			out.println("当前容器中的对象是:" + x);
		}
	}
	
	public static void main(String[] args){
		new ShowGenericType();
	}
}
//声明一个类用来演示泛型与非泛型
class Apple{
	private static long counter;
	private final long id = counter++;
	
	public long id(){
		return id;
	}
}

class Orange{
	private static long counter;
	private final long id = counter++;
	
	public long id(){
		return id;
	}
}

/////////////下面的类用来说明泛型的类型转换
class GrannySmith extends Apple{
	
}
class Gala extends Apple{
	
}
class Fuji extends Apple{
	
}
class Braeburn extends Apple{
	
}

总结

当我们使用集合保存了对象之后,再加上对象的序列化和反序列化操作,此时,我们就已经具备了编写单机应用的能力。此时,我们不用学什么数据库,因为持久操作对象的技巧我们已经掌握了。

我认为接着下面要学习的就是面向对象编程思想、设计模式才是Java的核心本质。而面向对象编程的思想在Java的Swing框架中体现得非常完美,因此,我建议需要快速掌握Java的小伙伴,接下来就可以看《Core Java™ Volume II–Advanced Features, Eighth Edition》一书,该书读全面讲述了Java 2的核心特性,这些特性是Java之后所有版本最核心的功能,它们一直都是Java不断演变的基础核心功能,它们一直都在于Java的内核之中,直到现在的Java 17版本也是!

最后

感觉有用的同学,请记得给大黍❤️关注+点赞+收藏+评论+转发❤️

作者:老九学堂—技术大黍

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。