1

Java

面向对象

首先:无论是面向对象还是面向过程他们都只是一个思想。

 

首先人们在解决一个问题的时候肯定是先分析出解决这个问题所需要的步骤然后一步一步的去实现并最终达到目的,软件开发也一样,在开发一个软件时,我们先分析解决问题的步骤,然后用函数把这些步骤一步一步的实现,然后在使用的时候一一调用,这样如果是一个简单的程序可能没什么,但是如果是一个大型的工程,他的需求就会非常的复杂甚至频繁的变更。而我们这种面向过程式的开发模式是以功能为目标来设计构造应用系统的,这种做法导致我们设计程序时,不得不将这一个个步骤映射到由功能模块组成的解空间中,我们的关注点在一个个的步骤,也就是一个个函数,这样当系统比较庞大时,如果一昧的去执著与这种模式,则会导致程序不利于复用,拓展,维护,比如现在有一个象棋程序,假如我要加入悔棋的功能,如果面向过程设计的话,那么从输入到判断到显示这一连串的步骤都要改动,甚至步骤之间的循序都要进行大规模调整,所以有人提出了一种模式,在这个模式里我们为数据和函数提供共同的独立内存空间,这些数据和函数可以作为模板以便在需要时创建类似模块的拷贝,这些拷贝之间相互独立。在这个开发模式中我们可以将一个问题分解为一些相互关联的子集,每个子集内部都包含了相关的数据和函数,这种为数据和函数提供共同的独立内存空间的行为我们称它为封装,这种开发思想我们叫它面向对象,这个模板我们称它为类,当我们需要使用时分配的某一个具体的内存空间的引用被称为对象,通过这个对象我们可以调用这个内存空间里的数据和函数,这些数据和函数分别被称为属性和方法,我们把相关的属性和方法封装到一个类里,在使用时这些类可以构造出独立的对象,相比于面向过程,拿吃饭来说:面向过程强调的是”吃饭”,”人”只是一个参数;面向对象强调的是”人”,”吃饭”只是一个动作.我们用对象来管理这些属性和动作。所以再解决上述悔棋问题时我们就不需要那么复杂,只用改动棋盘对象就行了,棋盘系统保存了红黑双方的棋谱,简单回溯就可以了,而显示和规则判断则不用顾及,同时整个对对象功能的调用顺序都没有变化,改动只是局部的,我们只需要改动棋盘那块内存里的数据,所以我们管这种以类和对象去去组织去管理数据和函数的这种开发方法称为面向对象。说白了一句话面向对象其实就是在系统庞大时提出的一种管理模块(代码的模块和内存的模块)的方式(功能的模块化,内存的独立性)。

 

百度百科:

面向对象是在结构化设计方法出现很多问题的情况下应运而生的。结构化设计方法求解问题的基本策略是从功能的角度审视问题域。它将应用程序看成实现某些特定任务的功能模块,其中子过程是实现某项具体操作的底层功能模块。在每个功能模块中,用数据结构描述待处理数据的组织形式,用算法描述具体的操作过程。面对日趋复杂的应用系统,这种开发思路在下面几个方面逐渐暴露了一些弱点。 [2] 

1.审视问题域的视角

在现实世界中存在的客体是问题域中的主角,所谓客体是指客观存在的对象实体和主观抽象的概念,他是人类观察问题和解决问题的主要目标。例如,对于一个学校学生管理系统来说,无论是简单还是复杂,始终是围绕学生和老师这两个客体实施。在自然界,每个客体都具有一些属性和行为,例如学生有学号、姓名、性别等属性,以及上课、考试、做实验等行为。因此,每个个体都可以用属性和行为来描述。 [2] 

通常人类观察问题的视角是这些客体,客体的属性反应客体在某一时刻的状态,客体的行为反映客体能从事的操作。这些操作附在客体之上并能用来设置、改变和获取客体的状态。任何问题域都有一系列的客体,因此解决问题的基本方式是让这些客体之间相互驱动、相互作用,最终使每个客体按照设计者的意愿改变其属性状态。 [2] 

结构化设计方法所采用的设计思路不是将客体作为一个整体,而是将依附于客体之上的行为抽取出来,以功能为目标来设计构造应用系统。这种做法导致在进行程序设计的时候,不得不将客体所构成的现实世界映射到由功能模块组成的解空间中,这种变换过程,不仅增加了程序设计的复杂程度,而且背离了人们观察问题和解决问题的基本思路。另外,再仔细思考会发现,在任何一个问题域中,客体是稳定的,而行为是不稳定的。例如,不管是国家图书馆,还是学校图书馆,还是国际图书馆,都会含有图书这个客体,但管理图书的方法可能是截然不同的。结构化设计方法将审视问题的视角定位于不稳定的操作之上,并将描述客体的属性和行为分开,使得应用程序的日后维护和扩展相当困难,甚至一个微小的变动,都会波及到整个系统。面对问题规模的日趋扩大、环境的日趋复杂、需求变化的日趋加快,将利用计算机解决问题的基本方法统一到人类解决问题的习惯方法之上,彻底改变软件设计方法与人类解决问题的常规方式扭曲的现象迫在眉睫,这是提出面向对象的首要原因。 [2] 

2.抽象级别

抽象是人类解决问题的基本法宝。良好的抽象策略可以控制问题的复杂程度,增强系统的通用性和可扩展性抽象主要包括过程抽象和数据抽象。结构化设计方法应用的是过程抽象。所谓过程抽象是将问题域中具有明确功能定义的操作抽取出来,并将其作为一个实体看待。这种抽象级别对于软件系统结构的设计显得有些武断,并且稳定性差,导致很难准确无误地设计出系统的每一个操作环节。一旦某个客体属性的表示方式发生了变化,就有可能牵扯到已有系统的很多部分。而数据抽象是较过程抽象更高级别的抽象方式,将描述客体的属性和行为绑定在一起,实现统一的抽象,从而达到对现实世界客体的真正模拟 [2] 

3.封装

封装是指将现实世界中存在的某个客体的属性与行为绑定在一起,并放置在一个逻辑单元内。该逻辑单元负责将所描述的属性隐藏起来,外界对客体内部属性的所有访问只能通过提供的用户接口实现。这样做既可以实现对客体属性的保护作用,又可以提高软件系统的可维护性。只要用户接口不改变,任何封装体内部的改变都不会对软件系统的其他部分造成影响。结构化设计方法没有做到客体的整体封装,只是封装了各个功能模块,而每个功能模块可以随意地对没有保护能力客体属性实施操作,并且由于描述属性的数据与行为被分割开来,所以一旦某个客体属性的表达方式发生了变化,或某个行为效果发生了改变,就有可能对整个系统产生影响。 [2] 

4.可重用性

重用性标识着软件产品的可复用能力,是衡量一个软件产品成功与否的重要标志。当今的软件开发行业,人们越来越追求开发更多的、更有通用性的可重用构件,从而使软件开发过程彻底改善,即从过去的语句级编写发展到现在的构件组装,从而提高软件开发效率,推动应用领域迅速扩展。然而,结构化程序设计方法的基本单位是模块,每个模块只是实现特定功能的过程描述,因此,它的可重用单位只能是模块。例如,在C语言编写程序时使用大量的标准函数。但对于今天的软件开发来说,这样的重用力度显得微不足道,而且当参与操作的某些数据类型发生变化时,就不能够再使用那些函数了。因此,渴望更大力度的可重用构件是如今应用领域对软件开发提出的新需求。 [2] 

上述弱点驱使人们寻求一种新的程序设计方法,以适应现代社会对软件开发的更高要求,面向对象由此产生。

 

继承

继承的特性

  • 子类拥有父类非private的属性,方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。
  • Java的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如A类继承B类,B类继承C类,所以按照关系就是C类是B类的父类,B类是A类的父类,这是java继承区别于C++继承的一个特性。
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。

 

继承关键字

继承可以使用 extends 和 implements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承object(这个类在 java.lang 包中,所以不需要 import)祖先类。

extends关键字

在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。

implements关键字

使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。

super 与 this 关键字

super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

this关键字:指向自己的引用。

final关键字

final 关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写:

实例变量也可以被定义为 final,被定义为 final 的变量不能被修改。被声明为 final 类的方法自动地声明为 final,但是实例变量并不是 final

构造器

子类是不继承父类的构造器(构造方法或者构造函数)的,它只是调用(隐式或显式)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。

如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器。

 

重写(Override)与重载(Overload)

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。

在面向对象原则里,重写意味着可以重写任何现有方法

方法的重写规则

  • 参数列表必须完全与被重写方法的相同;
  • 返回类型必须完全与被重写方法的返回类型相同;
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为final的方法不能被重写。
  • 声明为static的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个方法,则不能重写这个方法。

Super关键字的使用

当需要在子类中调用父类的被重写方法时,要使用super关键字。

重载(Overload)

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

最常用的地方就是构造器的重载。

重载规则:

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • (1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • (2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  1. 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

多态

 

多态是同一个行为具有多个不同表现形式或形态的能力。

多态就是同一个接口,使用不同的实例而执行不同操作

多态性是对象多种表现形式的体现。

现实中,比如我们按下 F1 键这个动作:

  • 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
  • 如果当前在 Word 下弹出的就是 Word 帮助;
  • 在 Windows 下弹出的就是 Windows 帮助和支持。

同一个事件发生在不同的对象上会产生不同的结果。

多态的优点

  • 1. 消除类型之间的耦合关系
  • 2. 可替换性
  • 3. 可扩充性
  • 4. 接口性
  • 5. 灵活性
  • 6. 简化性

多态存在的三个必要条件

  • 继承
  • 重写
  • 父类引用指向子类对象
  • 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
  • 多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理

多态的实现方式

方式一:重写:

这个内容已经在上一章节详细讲过,就不再阐述,详细可访问:Java 重写(Override)与重载(Overload)

方式二:接口

  • 1. 生活中的接口最具代表性的就是插座,例如一个三接头的插头都能接在三孔插座中,因为这个是每个国家都有各自规定的接口规则,有可能到国外就不行,那是因为国外自己定义的接口类型。
  • 2. java中的接口类似于生活中的接口,就是一些方法特征的集合,但没有方法的实现。具体可以看 java接口 这一章节的内容。

方式三:抽象类和抽象方法

 抽象类

 

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。

由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。

父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。

在Java中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。

抽象类

在Java语言中使用abstract class来定义抽象类

继承抽象类

尽管我们不能实例化一个Employee类的对象,但是如果我们实例化一个Salary类对象,该对象将从 Employee 类继承7个成员方法,且通过该方法可以设置或获取三个成员变量

抽象方法

如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。

Abstract关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。

抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。

声明抽象方法会造成以下两个结果:

如果一个类包含抽象方法,那么该类必须是抽象类。

任何子类必须重写父类的抽象方法,或者声明自身为抽象类。

继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。

如果Salary类继承了Employee类,那么它必须实现computePay()方法

抽象类总结规定

  • 1. 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
  • 2. 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
  • 3. 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
  • 4. 构造方法,类方法(用static修饰的方法)不能声明为抽象方法。
  • 5. 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

 

封装

 

在面向对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部份包装、隐藏起来的方法。

封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。

要访问该类的代码和数据,必须通过严格的接口控制。

封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。

适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。

封装的优点

  • 1. 良好的封装能够减少耦合。
  • 2. 类内部的结构可以自由修改。
  • 3. 可以对成员变量进行更精确的控制。
  • 4. 隐藏信息,实现细节。

实现Java封装的步骤

1. 修改属性的可见性来限制对属性的访问(一般限制为private)

2. 对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问,采用 this 关键字是为了解决实例变量(private String name)和局部变量(setName(String name)中的name变量)之间发生的同名的冲突

接口

接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。

接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。

除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。

接口与类相似点:

  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:

  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法。
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。

接口特性

  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

抽象类和接口的区别

  • 1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • 2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  • 4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

接口有以下特性:

  • 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
  • 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。
  • 接口中的方法都是公有的。

接口的实现

当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。

类使用implements关键字实现接口。在类声明中,Implements关键字放在class声明后面。

实现一个接口的语法,

重写接口中声明的方法时,需要注意以下规则:

  • 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
  • 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
  • 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。

在实现接口的时候,也要注意一些规则:

  • 一个类可以同时实现多个接口。
  • 一个类只能继承一个类,但是能实现多个接口。
  • 一个接口能继承另一个接口,这和类之间的继承比较相似。

 

接口的继承

一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法

Hockey接口自己声明了四个方法,从Sports接口继承了两个方法,这样,实现Hockey接口的类需要实现六个方法。

相似的,实现Football接口的类需要实现五个方法,其中两个来自于Sports接口

接口的多继承

在Java中,类的多继承是不合法,但接口允许多继承。

在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口

标记接口

最常用的继承接口是没有包含任何方法的接口。

标记接口是没有任何方法和属性的接口.它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。

标记接口作用:简单形象的说就是给某个对象打个标(盖个戳),使对象拥有某个或某些特权

没有任何方法的接口被称为标记接口。标记接口主要用于以下两种目的:

  • 建立一个公共的父接口:

正如EventListener接口,这是由几十个其他接口扩展的Java API,你可以使用一个标记接口来建立一组接口的父接口。例如:当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。

  • 向一个类添加数据类型:

这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型。

包(package)

为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间。

包的作用

  • 1、把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
  • 2、如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
  • 3、包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。

Java 使用包(package)这种机制是为了防止命名冲突,访问控制,提供搜索和定位类(class)、接口、枚举(enumerations)和注释(annotation)等。

那么它的路径应该是 net/java/util/Something.java 这样保存的。 package(包) 的作用是把不同的 java 程序分类保存,更方便的被其他 java 程序调用。

一个包(package)可以定义为一组相互联系的类型(类、接口、枚举和注释),为这些类型提供访问保护和命名空间管理的功能。

以下是一些 Java 中的包:

  • java.lang-打包基础的类
  • java.io-包含输入输出功能的函数

开发者可以自己把一组类和接口等打包,并定义自己的包。而且在实际开发中这样做是值得提倡的,当你自己完成类的实现之后,将相关的类分组,可以让其他的编程者更容易地确定哪些类、接口、枚举和注释等是相关的。

由于包创建了新的命名空间(namespace),所以不会跟其他包中的任何名字产生命名冲突。使用包这种机制,更容易实现访问控制,并且让定位相关类更加简单。

 

创建包

创建包的时候,你需要为这个包取一个合适的名字。之后,如果其他的一个源文件包含了这个包提供的类、接口、枚举或者注释类型的时候,都必须将这个包的声明放在这个源文件的开头。

包声明应该在源文件的第一行,每个源文件只能有一个包声明,这个文件中的每个类型都应用于它。

如果一个源文件中没有使用包声明,那么其中的类,函数,枚举,注释等将被放在一个无名的包(unnamed package)中。

import 关键字

为了能够使用某一个包的成员,我们需要在 Java 程序中明确导入该包。使用 "import" 语句可完成此功能

package 的目录结构

类放在包中会有两种主要的结果:

  • 包名成为类名的一部分,正如我们前面讨论的一样。
  • 包名必须与相应的字节码所在的目录结构相吻合。

下面是管理你自己 java 中文件的一种简单方式:

将类、接口等类型的源码放在一个文本中,这个文件的名字就是这个类型的名字,并以.java作为扩展名

多线程

基本概念

进程:正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

线程:是进程中的单个顺序控制流,是一条执行路径, 一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序。

大家注意两个词汇的区别:并行和并发。

           前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。

  后者是物理上同时发生,指在某一个时间点同时运行多个程序。

 

 

Java程序的运行原理:

   由java命令启动JVM,JVM启动就相当于启动了一个进程。

  接着有该进程创建了一个主线程去调用main方法。

  

  思考题:

jvm虚拟机的启动是单线程的还是多线程的?

     是多线程的。原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的。

 

需求:我们要实现多线程的程序。

 如何实现呢?

        由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。

  那么Java提供的类是什么呢?

   Thread

   通过查看API,我们知道了有2中方式实现多线程程序。

实现多线程的两种方式:

方式1:继承Thread类

  步骤

   A:自定义类MyThread继承Thread类。

   B:MyThread类里面重写run()?

   为什么是run()方法呢?

   C:创建对象

   D:启动线程

该类要重写run()方法,为什么呢?

不是类中的所有代码都需要被线程执行的。 而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。

获取和设置线程名称

获取线程对象的名称:

      public final String getName():获取线程的名称。

设置线程对象的名称:

 public final void setName(String name):设置线程的名称

  

  针对不是Thread类的子类中如何获取线程对象名称呢?

 public static Thread currentThread():返回当前正在执行的线程对象

           Thread.currentThread().getName() 

方式2:实现 Runnable 接口

 

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。

为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:

public void run()

你可以重写该方法,重要的是理解的 run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。

在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。

Thread 定义了几个构造方法,下面的这个是我们经常使用的:

Thread(Runnable threadOb,String threadName);

这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。

新线程创建之后,你调用它的 start() 方法它才会运行。

void start();

下面是一个创建线程并开始让它执行的实例:

实例

线程控制

 

线程休眠:

 

 

守护线程

public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。

当正在运行的线程都是守护线程时,java认为程序执行结束,Java 虚拟机退出。 该方法必须在启动线程前调用。

public class ThreadDaemonDemo {

public static void main(String[] args) {

ThreadDaemon td1 = new ThreadDaemon();

ThreadDaemon td2 = new ThreadDaemon();

 

td1.setName("关羽");

td2.setName("张飞");

 

// 设置收获线程

td1.setDaemon(true);

td2.setDaemon(true);

 

td1.start();

td2.start();

 

Thread.currentThread().setName("刘备");

for (int x = 0; x < 5; x++) {

System.out.println(Thread.currentThread().getName() + ":" + x);

}

}

}

加入

public final void join():等待该线程终止。 一个程序若执行此操作,则改线程执行完毕其他线程方可执行。

public class ThreadJoinDemo {

public static void main(String[] args) {

ThreadJoin tj1 = new ThreadJoin();

ThreadJoin tj2 = new ThreadJoin();

ThreadJoin tj3 = new ThreadJoin();

 

tj1.setName("李渊");

tj2.setName("李世民");

tj3.setName("李元霸");

 

tj1.start();

try {

tj1.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

 

tj2.start();

tj3.start();

}

}

 

 

优先级

线程默认优先级是5。线程优先级的范围是:1-10。线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。

 获取线程对象的优先级

   public final int getPriority():返回线程对象的优先级

 设置线程对象的优先级

   public final void setPriority(int newPriority):更改线程的优先级。

  

 

  

  IllegalArgumentException:非法参数异常。

  抛出的异常表明向方法传递了一个不合法或不正确的参数。

中断线程

public final void stop():让线程停止,过时了,但是还可以使用。停止线程中的所有操作,包括异常。

public void interrupt():中断线程。 把线程的状态终止,可以抛出一个InterruptedException异常,并且后面的代码仍然可以执行,任然可以继续睡,但不会抛出异常,即异常只会捕捉一次

public class ThreadStopDemo {

public static void main(String[] args) {

ThreadStop ts = new ThreadStop();

ts.start();

 

// 你超过三秒不醒过来,我就干死你

try {

Thread.sleep(3000);

// ts.stop();

ts.interrupt();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

 

礼让线程

public static void yield():暂停当前正在执行的线程对象,并执行其他线程。

  让多个线程的执行更和谐,但是不能靠它保证一人一次。

public class ThreadYield extends Thread {

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(getName() + ":" + x);

Thread.yield();

}

}

}

 

方式2:实现Runnable接口

  步骤:

   A:自定义类MyRunnable实现Runnable接口

   B:重写run()方法

   C:创建MyRunnable类的对象

  D:创建Thread类的对象,并把C步骤的对象作为构造参数传递

 

public class MyRunnableDemo {

public static void main(String[] args) {

// 创建MyRunnable类的对象

MyRunnable my = new MyRunnable();

 

// 创建Thread类的对象,并把C步骤的对象作为构造参数传递

// Thread(Runnable target)

// Thread t1 = new Thread(my);

// Thread t2 = new Thread(my);

// t1.setName("林青霞");

// t2.setName("刘意");

 

// Thread(Runnable target, String name)

Thread t1 = new Thread(my, "林青霞");

Thread t2 = new Thread(my, "刘意");

 

t1.start();

t2.start();

}

}

售票程序:

第一种继承类:

public class SellTicket extends Thread {

 

// 定义100张票

// private int tickets = 100;

// 为了让多个线程对象共享这100张票,我们其实应该用静态修饰

private static int tickets = 100;

 

@Override

public void run() {

// 定义100张票

// 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面

// int tickets = 100;

 

// 是为了模拟一直有票

while (true) {

if (tickets > 0) {

System.out.println(getName() + "正在出售第" + (tickets--) + "张票");

}

}

}

}

第二种实现接口:

public class SellTicket implements Runnable {

// 定义100张票

private int tickets = 100;

 

@Override

public void run() {

while (true) {

if (tickets > 0) {

System.out.println(Thread.currentThread().getName() + "正在出售第"

+ (tickets--) + "张票");

}

}

}

 

} 

加入延迟:

之后出现同一张票卖多次和负数票问题:

public class SellTicket implements Runnable {

// 定义100张票

private int tickets = 100;

 

@Override

public void run() {

while (true) {

// t1,t2,t3三个线程

// 这一次的tickets = 1;

if (tickets > 0) {

// 为了模拟更真实的场景,我们稍作休息

try {

Thread.sleep(100); //t1进来了并休息,t2进来了并休息,t3进来了并休息,

} catch (InterruptedException e) {

e.printStackTrace();

}

 

System.out.println(Thread.currentThread().getName() + "正在出售第"

+ (tickets--) + "张票");

//窗口1正在出售第1张票,tickets=0

//窗口2正在出售第0张票,tickets=-1

//窗口3正在出售第-1张票,tickets=-2

}

}

}

 

} 

 

问题分析:

A:相同的票卖了多次

   一个CPU的一次操作必须是原子性的,CPU的操作是原子性的(所谓原子性操作即是单一操作,比如System.out.println(i++);执行了打印和自增两个原子性操作,那么整条语句就不是一次原子性应操作,所以在执行整条语句时执行权就有可能被抢,系统转而执行其他程序,此次混乱是发生在输出票数和自增之间的时间段)

B:出现了负数票

  随机性和延迟导致的(三者都已经在票数不为零时进如语句块,即都已经在判断条件时通过了判断,而内部售票算法又不具备校验性,所以在执行了减法操作之后就出现了负数票,但不会由后续线程进入语句块,因为票数已经不满足条件)

解决问题:

同步代码块

此种解决方式要注意锁的问题:

一个语句块只有用一个锁才可以起到同步作用,例如注释部分的代码就不正确,因为不是同一个锁,注意使用此种方式解决问题需要在多线程的环境下,且此锁对象为任意对象。

好处:同步的出现解决了多线程的安全问题。

弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

public class SellTicket implements Runnable {

// 定义100张票

private int tickets = 100;

//创建锁对象

private Object obj = new Object();

 

// @Override

// public void run() {

// while (true) {

// synchronized(new Object()){

// if (tickets > 0) {

// try {

// Thread.sleep(100);

// } catch (InterruptedException e) {

// e.printStackTrace();

// }

// System.out.println(Thread.currentThread().getName() + "正在出售第"

// + (tickets--) + "张票");

// }

// }

// }

// }

 

@Override

public void run() {

while (true) {

synchronized (obj) {

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票");

}

}

}

}

}

 

同步方法:

把同步关键字加在方法上, 锁对象为this,例如此时run方法中SellTicket.class处是其他就会出现线程安全问题,证明同步方法的所对象为this 。

 

public class SellTicket implements Runnable {

 

// 定义100张票

private static int tickets = 100;

 

// 定义同一把锁

private Object obj = new Object();

private Demo d = new Demo();

 

private int x = 0;

 

 

 

@Override

public void run() {

while (true) {

if(x%2==0){

synchronized (SellTicket.class) {

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票 ");

}

}

}else {

 

 

sellTicket();

 

}

x++;

}

}

 

 

 

private synchronized void sellTicket() {

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票 ");

}

}

 

 

}

 

class Demo {

 

 

}

静态方法

静态方法的锁对象是类的字节码文件对象

 

private static synchronized void sellTicket() {

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票 ");

}

 

}

 

 

 

线程安全类:

// 线程安全的类

StringBuffer sb = new StringBuffer();

Vector<String> v = new Vector<String>();

Hashtable<String, String> h = new Hashtable<String, String>();

 

// Vector是线程安全的时候才去考虑使用的,但是我还说过即使要安全,我也不用你

// 那么到底用谁呢?

// public static <T> List<T> synchronizedList(List<T> list)

List<String> list1 = new ArrayList<String>();// 线程不安全

List<String> list2 = Collections

.synchronizedList(new ArrayList<String>()); // 线程安全

lock()与unlock()的使用

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

  Lock:

   void lock(): 获取锁。

   void unlock():释放锁。  

  ReentrantLock是Lock的实现类.

public class SellTicket implements Runnable {

 

// 定义票

private int tickets = 100;

 

// 定义锁对象

private Lock lock = new ReentrantLock();

 

@Override

public void run() {

while (true) {

try {

// 加锁

lock.lock();

if (tickets > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()

+ "正在出售第" + (tickets--) + "张票");

}

} finally {

// 释放锁

lock.unlock();

}

}

}

 

} 

死锁问题:

同步的弊端:效率低,容易产生死锁

死锁:两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。

 

比如此时如下代码:假如一个线程进来进过a行至b处时,另一个线程已经进来获得 了B锁,所以前一个线程就会等待B锁的释放,而当后一线程行至d处时发现A锁未释放,所以会等待A锁的释放,如此两者相互等待,便造成了死锁。

 

public class MyLock {

// 创建两把锁对象

public static final Object objA = new Object();

public static final Object objB = new Object();

} 

public class DieLock extends Thread {

 

private boolean flag;

 

public DieLock(boolean flag) {

this.flag = flag;

}

 

@Override

public void run() {

if (flag) {

//a

synchronized (MyLock.objA) {

System.out.println("if objA");//b

synchronized (MyLock.objB) {

System.out.println("if objB");

}

}

} else {

//c

synchronized (MyLock.objB) {

System.out.println("else objB");//d

synchronized (MyLock.objA) {

System.out.println("else objA");

}

}

}

}

} 

生产者消费者模型:

注意:不同种类的线程加的锁必须是同一把。 比如此时的set和get线程的锁同时为student.

直接写会出现的问题  

    为了数据的效果好一些,我加入了循环和判断,给出不同的值,这个时候产生了新的问题:同一个数据出现多次(原因:CPU的一点点时间片的执行权,就足够你执行很多次),姓名和年龄不匹配(线程运行的随机性)

 解决方案:

   加锁:不同种类的线程都要加锁且不同种类的线程加的锁必须是同一把。

public class GetThread implements Runnable {

private Student s;

 

public GetThread(Student s) {

this.s = s;

}

 

@Override

public void run() {

while (true) {

synchronized (s) {

System.out.println(s.name + "---" + s.age);

}

}

}

}

public class SetThread implements Runnable {

 

private Student s;

private int x = 0;

 

public SetThread(Student s) {

this.s = s;

}

 

@Override

public void run() {

while (true) {

synchronized (s) {

if (x % 2 == 0) {

s.name = "林青霞";//刚走到这里,就被别人抢到了执行权

s.age = 27;

} else {

s.name = "刘意"; //刚走到这里,就被别人抢到了执行权

s.age = 30;

}

x++;

}

}

}

}

 

 public class Student {

String name;

int age;

}

 一次一大片问题:

解决:等待唤醒机制。

等待唤醒:

     Object类中提供了三个方法:

   wait():等待

   notify():唤醒单个线程

   notifyAll():唤醒所有线程

   为什么这些方法不定义在Thread类中呢?

   这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。

  所以,这些方法必须定义在Object类中。

 

public class Student {

String name;

int age;

boolean flag; // 默认情况是没有数据,如果是true,说明有数据

} 

public class GetThread implements Runnable {

private Student s;

 

public GetThread(Student s) {

this.s = s;

}

 

@Override

public void run() {

while (true) {

synchronized (s) {

if(!s.flag){

try {

s.wait(); //t2就等待了。立即释放锁。将来醒过来的时候,是从这里醒过来的时候

} catch (InterruptedException e) {

e.printStackTrace();

}

}

 

System.out.println(s.name + "---" + s.age);

//林青霞---27

//刘意---30

 

//修改标记

s.flag = false;

//唤醒线程

s.notify(); //唤醒t1

}

}

}

}

 

public class SetThread implements Runnable {

 

private Student s;

private int x = 0;

 

public SetThread(Student s) {

this.s = s;

}

 

@Override

public void run() {

while (true) {

synchronized (s) {

//判断有没有

if(s.flag){

try {

s.wait(); //t1等着,释放锁

} catch (InterruptedException e) {

e.printStackTrace();

}

}

 

if (x % 2 == 0) {

s.name = "林青霞";

s.age = 27;

} else {

s.name = "刘意";

s.age = 30;

}

x++; //x=1

 

//修改标记

s.flag = true;

//唤醒线程

s.notify(); //唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。

}

//t1有,或者t2有

}

}

}

 

 public class StudentDemo {

public static void main(String[] args) {

//创建资源

Student s = new Student();

//设置和获取的类

SetThread st = new SetThread(s);

GetThread gt = new GetThread(s);

//线程类

Thread t1 = new Thread(st);

Thread t2 = new Thread(gt);

//启动线程

t1.start();

t2.start();

}

} 

 

最终版把student的成员私有化。

public class Student {

private String name;

private int age;

private boolean flag; // 默认情况是没有数据,如果是true,说明有数据

 

public synchronized void set(String name, int age) {

// 如果有数据,就等待

if (this.flag) {

try {

this.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

 

// 设置数据

this.name = name;

this.age = age;

 

// 修改标记

this.flag = true;

this.notify();

}

 

public synchronized void get() {

// 如果没有数据,就等待

if (!this.flag) {

try {

this.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

 

// 获取数据

System.out.println(this.name + "---" + this.age);

 

// 修改标记

this.flag = false;

this.notify();

}

}

 

 public class SetThread implements Runnable {

 

private Student s;

private int x = 0;

 

public SetThread(Student s) {

this.s = s;

}

 

@Override

public void run() {

while (true) {

if (x % 2 == 0) {

s.set("林青霞", 27);

} else {

s.set("刘意", 30);

}

x++;

}

}

}

public class GetThread implements Runnable {

private Student s;

 

public GetThread(Student s) {

this.s = s;

}

 

@Override

public void run() {

while (true) {

s.get();

}

}

} 

public class StudentDemo {

public static void main(String[] args) {

//创建资源

Student s = new Student();

 

//设置和获取的类

SetThread st = new SetThread(s);

GetThread gt = new GetThread(s);

 

//线程类

Thread t1 = new Thread(st);

Thread t2 = new Thread(gt);

 

//启动线程

t1.start();

t2.start();

}

} 

线程组:

线程组: 把多个线程组合到一起,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。可以按组管理

public class MyRunnable implements Runnable {

 

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":" + x);

}

}

 

}

 

public class ThreadGroupDemo {

public static void main(String[] args) {

// method1();

 

// 我们如何修改线程所在的组呢?

// 创建一个线程组

// 创建其他线程的时候,把其他线程的组指定为我们自己新建线程组

method2();

 

// t1.start();

// t2.start();

}

 

private static void method2() {

// ThreadGroup(String name)

ThreadGroup tg = new ThreadGroup("这是一个新的组");

 

MyRunnable my = new MyRunnable();

// Thread(ThreadGroup group, Runnable target, String name)

Thread t1 = new Thread(tg, my, "林青霞");

Thread t2 = new Thread(tg, my, "刘意");

 

System.out.println(t1.getThreadGroup().getName());

System.out.println(t2.getThreadGroup().getName());

 

//通过组名称设置后台线程,表示该组的线程都是后台线程

tg.setDaemon(true);

}

 

private static void method1() {

MyRunnable my = new MyRunnable();

Thread t1 = new Thread(my, "林青霞");

Thread t2 = new Thread(my, "刘意");

// 我不知道他们属于那个线程组,我想知道,怎么办

// 线程类里面的方法:public final ThreadGroup getThreadGroup()

ThreadGroup tg1 = t1.getThreadGroup();

ThreadGroup tg2 = t2.getThreadGroup();

// 线程组里面的方法:public final String getName()

String name1 = tg1.getName();

String name2 = tg2.getName();

System.out.println(name1);

System.out.println(name2);

// 通过结果我们知道了:线程默认情况下属于main线程组

// 通过下面的测试,你应该能够看到,默任情况下,所有的线程都属于同一个组

System.out.println(Thread.currentThread().getThreadGroup().getName());

}

}

线程池

线程池的好处:线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

 

实现线程池:

   A:创建一个线程池对象,控制要创建几个线程对象。

  public static ExecutorService newFixedThreadPool(int nThreads)

   B:这种线程池的线程可以执行:

  可以执行Runnable对象或者Callable对象代表的线程

   做一个类实现Runnable接口。

   C:调用如下方法即可

   Future<?> submit(Runnable task)

  <T> Future<T> submit(Callable<T> task)

  D:我就要结束,可以吗?

  可以。

public class MyRunnable implements Runnable {

 

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":" + x);

}

}

 

}

 

public class ExecutorsDemo {

public static void main(String[] args) {

// 创建一个线程池对象,控制要创建几个线程对象。

// public static ExecutorService newFixedThreadPool(int nThreads)

ExecutorService pool = Executors.newFixedThreadPool(2);

 

// 可以执行Runnable对象或者Callable对象代表的线程

pool.submit(new MyRunnable());

pool.submit(new MyRunnable());

 

//结束线程池

pool.shutdown();

}

}

多线程实现的方式3:

A:创建一个线程池对象,控制要创建几个线程对象。

   public static ExecutorService newFixedThreadPool(int nThreads)

 B:这种线程池的线程可以执行:

   可以执行Runnable对象或者Callable对象代表的线程

   做一个类实现Runnable接口。

  C:调用如下方法即可

   Future<?> submit(Runnable task)

  <T> Future<T> submit(Callable<T> task)

 D:我就要结束,可以吗?

  可以。

 

 

import java.util.concurrent.Callable;

 

//Callable:是带泛型的接口。

//这里指定的泛型其实是call()方法的返回值类型。

public class MyCallable implements Callable {

 

@Override

public Object call() throws Exception {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":" + x);

}

return null;

}

 

} 

 

public class CallableDemo {

public static void main(String[] args) {

//创建线程池对象

ExecutorService pool = Executors.newFixedThreadPool(2);

 

//可以执行Runnable对象或者Callable对象代表的线程

pool.submit(new MyCallable());

pool.submit(new MyCallable());

 

//结束

pool.shutdown();

}

}

求和案例

import java.util.concurrent.Callable;

 

/*

 * 线程求和案例

 */

 

public class MyCallable implements Callable<Integer> {

 

private int number;

 

public MyCallable(int number) {

this.number = number;

}

 

@Override

public Integer call() throws Exception {

int sum = 0;

for (int x = 1; x <= number; x++) {

sum += x;

}

return sum;

}

 

}

 

import java.util.concurrent.ExecutionException;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.Future;

 

public class CallableDemo {

public static void main(String[] args) throws InterruptedException, ExecutionException {

// 创建线程池对象

ExecutorService pool = Executors.newFixedThreadPool(2);

// 可以执行Runnable对象或者Callable对象代表的线程

Future<Integer> f1 = pool.submit(new MyCallable(100));

Future<Integer> f2 = pool.submit(new MyCallable(200));

// V get()

Integer i1 = f1.get();

Integer i2 = f2.get();

System.out.println(i1);

System.out.println(i2);

// 结束

pool.shutdown();

}

}

 

匿名内部类的方式:

格式: new 类名或者接口名() {重写方法;};本质:是该类或者接口的子类对象。

 

public static void main(String[] args) {

// 继承Thread类来实现多线程

new Thread() {

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":"

+ x);

}

}

}.start();

 

// 实现Runnable接口来实现多线程

new Thread(new Runnable() {

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println(Thread.currentThread().getName() + ":"

+ x);

}

}

}) {

}.start();

 

// 更有难度的,走的是world部分,即此时是按继承Thread类来执行,但是不会报错。

new Thread(new Runnable() {

@Override

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println("hello" + ":" + x);

}

}

}) {

public void run() {

for (int x = 0; x < 100; x++) {

System.out.println("world" + ":" + x);

}

}

}.start();

}

 

 

方式3:通过 Callable 和 Future 创建线程

 

1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

创建线程的三种方式的对比

1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

集合

框架结构图

 

可看出,接口主要有Collection和Map两大主线,其中Collection又有List和Set两个分支。List是一个有序的队列,每一个元素都有它的索引;而Set是一个不允许有重复元素的集合。Map是一个key-value键值对映射,AbstractMap是个抽象类,它实现了Map接口中的大部分API。Hashtable继承于Dictionary,也实现了Map接口。

 

ArrayList

 

 

 

ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable接口。ArrayList不是线程安全的。

 

 

 

ArrayList包含两个重要的对象:elementData和size。

 

size是动态数组的实际大小,这里主要说说elementData。它是Object[]类型的数组,保存了添加到ArrayList中的元素。elementData是个动态数组,可通过构造函数ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量= (原始容量x3) / 2 + 1。

 

ArrayList转对象数组方式:Integer[] newText = (Integer[])v.toArray(new Integer[0]);

 

 

 

 

 

LinkedList

 

 

 

LinkedList是继承于AbstractSequentialList的双向链表,可被当作堆栈、队列或双端队列进行操作。实现了List,Deque,Cloneable,java.io.Serializable接口。LinkedList不是线程安全的。

 

 

 

LinkedList包含两个重要的成员:header和size。

 

 

 

size是双向链表中节点的个数,而header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous、next和element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。

 

 

 

LinkedList是通过双向链表去实现的,因此它的顺序访问会非常高效,而随机访问效率比较低。它的索引是通过一个计数索引值来实现的。例如,当调用get(int location)时,首先会比较“location”和“双向链表长度的1/2”;若前者大,则从链表头开始往后查找,直到location位置;否则,从链表末尾开始先前查找,直到location位置。

 

 

 

 

 

Vector

 

 

 

Vector是矢量队列,继承于AbstractList,实现了List、RandomAccess和Cloneable接口。Vector是线程安全的。

 

 

 

Vector包含了3个成员变量:elementData、elementCount和capacityIncrement。

 

elementData是Object[]类型的数组,它保存了添加到Vector中的元素。如果初始化Vector时,没指定elementData的大小,则默认大小10。随着Vector中元素的增加,Vector的容量也会动态增长。

 

elementCount是动态数组的实际大小。

 

capacityIncrement是动态数组的增长系数。如果在创建Vector时指定了capacityIncrement的大小,则每次当Vector中动态数组容量增加的大小都是capacityIncrement。

 

 

 

Vector的线程安全,是通过基本在所有方法上都加了synchronized关键字实现的。

 

 

 

 

 

HashMap

 

 

 

HashMap是一个散列表,存储的内容是key-value键值对映射。继承于AbstractMap,实现了Map、Cloneable和java.io.Serializable接口。HashMap不是线程安全的。HashMap的key、value都可以为null,映射不是有序的。

 

 

 

HashMap包括几个重要的成员变量:table、size、threshold、loadFactor和modCount。

 

 

 

table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的key-value键值对都是存储在Entry数组中的。

 

 

 

size是HashMap的大小,是HashMap保存的键值对的数量。

 

 

 

threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold=容量 * 负载因子,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。

 

 

 

loadFactor即负载因子,哈希表在其容量自动增加之前可以达到多满的一种尺度。默认为0.75。

 

 

 

modCount是用来实现fail-fast机制的。

 

 

 

HashMap是先调用Object的hashCode方法(native实现),然后indexFor(与table数组长度-1取模)得到对应数组下标。

 

 

 

 

 

 

 

Hash冲突:当put()操作的key与entry数组有冲突时,新的entry仍然会被安放在对应的索引下标内,替换原有的值。同时,为了保证旧值不丢失,会将新的entry的next指向旧值。这就实现了在一个数组索引空间内存放多个值。为了避免冲突,需要将hashCode()和hash()方法实现的足够好,尽可能减少冲突的产生。

 

 

 

HashMap将key为null的元素都放在table的位置0处。

 

 

 

 

 

HashTable

 

 

 

HashTable也是一个散列表,存储的内容是键值对(key-value)映射。继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。Hashtable是线程安全的。Hashtable的key、value都不可以为null,映射不是有序的。

 

 

 

HashMap包括几个重要的成员变量:table、size、threshold、loadFactor和modCount。基本同HashMap一致,不再赘述。

 

 

 

HashTable的线程安全,是通过基本在所有方法上都加了synchronized关键字实现的。

 

 

 

LinkedHashMap

 

 

 

 

LinkedHashMap是一个有序的key-value集合,继承于HashMap,实现了Map接口。按插入的顺序排序,不是线程安全的,不允许使用 null 值和 null 键。

 

 

 

LinkedHashMap底层使用哈希表与双向链表来保存所有元素。重新定义了数组中保存的元素 Entry,该 Entry 除了保存当前对象的引用外,还保存了其上一个元素 before 和下一个元素 after 的引用,从而在哈希表的基础上又构成了双向链接列表。

 

 

 

初始化时增加 accessOrder 参数,默认为 false,代表按照插入顺序进行迭代;设置为 true时代表以访问顺序进行迭代。

 

 

 

在进行put等元素操作的时候,LinkedHashMap会对自己维护的双向链表执行相同的操作。

 

 

 

TreeMap

 

 

 

TreeMap是一个有序的key-value集合,通过红黑树实现。继承于AbstractMap,实现了NavigableMap(支持一系列的导航方法)、Cloneable和java.io.Serializable接口。排列顺序根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap不是线程安全的。

 

 

 

TreeMap包含几个重要的成员变量: root、size和comparator。

 

 

 

root是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的。

 

 

 

size是红黑数中节点的个数。

 

 

 

comparator是键值排序的比较实现。

 

 

 

 

 

HashSet

 

 

 

 

HashSet是一个没有重复元素的集合,由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素。HashSet不是线程安全的。

 

 

 

 

 

TreeSet

 

 

 

 

TreeSet是一个有序的集合,作用是提供有序的Set集合。继承于AbstractSet抽象类,实现了NavigableSet<E>(支持一系列的导航方法)、Cloneable和java.io.Serializable接口。TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。TreeSet不是线程安全的。

泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性。 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<E>)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。

有界的类型参数:

可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。

要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。

实例

下面的例子演示了"extends"如何使用在一般意义上的意思"extends"(类)或者"implements"(接口)。该例子中的泛型方法返回三个可比较对象的最大值。

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

实例

如下实例演示了我们如何定义一个泛型类:

类型通配符

1、类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List<String>,List<Integer> 等所有List<具体类型实参>的父类。

解析: 因为getData()方法的参数是List类型的,所以name,age,number都可以作为这个方法的实参,这就是通配符的作用

2、类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。

解析: 在(//1)处会出现错误,因为getUperNumber()方法中的参数已经限定了参数泛型上限为Number,所以泛型为String是不在这个范围之内,所以会报错

3、类型通配符下限通过形如 List<? super Number>来定义,表示类型只能接受Number及其三层父类类型,如Objec类型的实例。

 

 

类反射

 

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.

 

要想理解反射的原理,首先要了解什么是类型信息。Java让我们在运行时识别对象和类的信息,主要有2种方式:一种是传统的RTTI,它假定我们在编译时已经知道了所有的类型信息;另一种是反射机制,它允许我们在运行时发现和使用类的信息。

获取构造方法

getConstructors

getDeclaredConstructors

创建对象

newInstance()

con.newInstance(“zhangsan", 20);

获取所有成员

getFields,getDeclaredFields

获取单个成员

getField,getDeclaredField

修改成员的值

set(Object obj,Object value)
   将指定对象变量上此 Field 对象表示的字段设置为指定的新值。

获取所有方法

getMethods

getDeclaredMethods

获取单个方法

getMethod

getDeclaredMethod

暴力访问

method.setAccessible(true);

 

1、Class对象

  理解RTTI在Java中的工作原理,首先需要知道类型信息在运行时是如何表示的,这是由Class对象来完成的,它包含了与类有关的信息。Class对象就是用来创建所有“常规”对象的,Java使用Class对象来执行RTTI,即使你正在执行的是类似类型转换这样的操作。

  每个类都会产生一个对应的Class对象,也就是保存在.class文件。所有类都是在对其第一次使用时,动态加载到JVM的,当程序创建一个对类的静态成员的引用时,就会加载这个类。Class对象仅在需要的时候才会加载,static初始化是在类加载时进行的。

public class TestMain {

    public static void main(String[] args) {

        System.out.println(XYZ.name);

    }

}

class XYZ {

    public static String name = "luoxn28";

 

    static {

        System.out.println("xyz静态块");

    }

 

    public XYZ() {

        System.out.println("xyz构造了");

    }

}

 类加载器首先会检查这个类的Class对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找对应的.class文件。

  想在运行时使用类型信息,必须获取对象(比如类Base对象)的Class对象的引用,使用功能Class.forName(“Base”)可以实现该目的,或者使用base.class。注意,有一点很有趣,使用功能”.class”来创建Class对象的引用时,不会自动初始化该Class对象,使用forName()会自动初始化该Class对象。为了使用类而做的准备工作一般有以下3个步骤:

  • 加载:由类加载器完成,找到对应的字节码,创建一个Class对象
  • 链接:验证类中的字节码,为静态域分配空间
  • 初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块

 

public class Base {

    static int num = 1;

    

    static {

        System.out.println("Base " + num);

    }

}public class Main {

    public static void main(String[] args) {

        // 不会初始化静态块

        Class clazz1 = Base.class;

        System.out.println("------");

        // 会初始化

        Class clazz2 = Class.forName("zzz.Base");

    }

}

2、类型转换前先做检查

编译器将检查类型向下转型是否合法,如果不合法将抛出异常。向下转换类型前,可以使用instanceof判断。

class Base { }class Derived extends Base { }

public class Main {

    public static void main(String[] args) {

        Base base = new Derived();

        if (base instanceof Derived) {

            // 这里可以向下转换了

            System.out.println("ok");

        }

        else {

            System.out.println("not ok");

        }

    }

}

3、反射:运行时类信息

  如果不知道某个对象的确切类型,RTTI可以告诉你,但是有一个前提:这个类型在编译时必须已知,这样才能使用RTTI来识别它。Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含Field、Method和Constructor类,这些类的对象由JVM在启动时创建,用以表示未知类里对应的成员。这样的话就可以使用Contructor创建新的对象,用get()和set()方法获取和修改类中与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()等许多便利的方法,以返回表示字段、方法、以及构造器对象的数组,这样,对象信息可以在运行时被完全确定下来,而在编译时不需要知道关于类的任何事情。

  反射机制并没有什么神奇之处,当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:

  • RTTI,编译器在编译时打开和检查.class文件
  • 反射,运行时打开和检查.class文件

public class Person implements Serializable {

 

    private String name;

    private int age;// get/set方法}public static void main(String[] args) {

    Person person = new Person("luoxn28", 23);

    Class clazz = person.getClass();

 

    Field[] fields = clazz.getDeclaredFields();

    for (Field field : fields) {

        String key = field.getName();

        PropertyDescriptor descriptor = new PropertyDescriptor(key, clazz);

        Method method = descriptor.getReadMethod();

        Object value = method.invoke(person);

 

        System.out.println(key + ":" + value);

 

    }

}

以上通过getReadMethod()方法调用类的get函数,可以通过getWriteMethod()方法来调用类的set方法。通常来说,我们不需要使用反射工具,但是它们在创建动态代码会更有用,反射在Java中用来支持其他特性的,例如对象的序列化和JavaBean等。

4、动态代理

  代理模式是为了提供额外或不同的操作,而插入的用来替代”实际”对象的对象,这些操作涉及到与”实际”对象的通信,因此代理通常充当中间人角色。Java的动态代理比代理的思想更前进了一步,它可以动态地创建并代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的策略。以下是一个动态代理示例:

 public interface Interface {

    void doSomething();

    void somethingElse(String arg);

}public class RealObject implements Interface {

    public void doSomething() {

        System.out.println("doSomething.");

    }

    public void somethingElse(String arg) {

        System.out.println("somethingElse " + arg);

    }

}

动态代理对象处理器:

public class DynamicProxyHandler implements InvocationHandler {

    private Object proxyed;

    

    public DynamicProxyHandler(Object proxyed) {

        this.proxyed = proxyed;

    }

    

    @Override

    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        System.out.println("代理工作了.");

        return method.invoke(proxyed, args);

    }

}

测试类:

public class Main {

    public static void main(String[] args) {

        RealObject real = new RealObject();

        Interface proxy = (Interface) Proxy.newProxyInstance(

                Interface.class.getClassLoader(), new Class[] {Interface.class},

                new DynamicProxyHandler(real));

        

        proxy.doSomething();

        proxy.somethingElse("luoxn28");

    }

}

 通过调用Proxy静态方法Proxy.newProxyInstance()可以创建动态代理,这个方法需要得到一个类加载器,一个你希望该代理实现的接口列表(不是类或抽象类),以及InvocationHandler的一个实现类。动态代理可以将所有调用重定向到调用处理器,因此通常会调用处理器的构造器传递一个”实际”对象的引用,从而将调用处理器在执行中介任务时,将请求转发。

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值