数组和集合的哲学思考

1       数组和集合的哲学认知

在汉语大词典中集合的名词解释是(1)在现实生活中,许多分散的人和物聚在一起;(2)在数学上指若干具有共同属性的事物的总体。如全部整数就成一个整数的集合。而数组在汉语大词典中却没有解释,我们借用以下的解释作为数组的名词解释,数组是一组具有相同类型和名称的变量的集合。这些变量称为数组的元素,每个数组元素都有一个编号,这个编号叫做下标,我们可以通过下标来区别这些元素。数组元素的个数有时也称之为数组的长度。数组是属于集合的,是集合中的一个分类,数组是集合中的一种特殊形式。

数组和集合总体的形的抽象也。在现实生活中集体是由个体组成、线是由点组成、平台是由线组成的,为了研究这些个体聚集而成的物质,我们就需要一种对这种物质的抽象形式,即数组或集合。研究数组和集合,可以帮助我们解决求平均值、最大最小、排序、查找等统计类的问题。

数组和集合是统计的基础。统计总体简称总体是我们要调查或统计某一现象全部数据的集合。总体是一个简化的概念,它可以分为自然总体和测量总体。所谓自然总体就是由客观存在的具有相同性质的许多个别事物构成的整体;自然总体中的个体通常都具有多种属性,我们把个体所具有某种共同那个属性的数值的整体称为一个测量总体。总体必须具备的三个特性,即大量性、同质性、变异性。

分类是集合的前提,集合是分类的结果,分类标准是按事物的属性。集合就是按事物某个属性分类的结果体。“分”即鉴定、描述和命名,“类”即归类,按一定秩序排列类群,也是系统演化。把无规律的事物分为有规律的,按照不同的特点分类事物,使事物按照分类对应规则来处理。

数组和集合的理论基础是集合论。集合论或集论是研究集合(由一堆抽象物件构成的整体)的数学理论,包含集合、元素和成员关系等最基本数学概念。在大多数现代数学的公式化中,集合论提供了要如何描述数学物件的语言。集合论和逻辑与一阶逻辑共同构成了数学的公理化基础,以未定义的“集合”与“集合成员”等术语来形式化地建构数学物件。在朴素集合论中,集合是被当做一堆物件构成的整体之类的自证概念。 在公理化集合论中,集合和集合成员并不直接被定义,而是先规范可以描述其性质的一些公理。在此一想法之下,集合和集合成员是有如在欧式几何中的点和线,而不被直接定义。所以可以把集合论(包括集合的定义和运算)看作是逻辑的形式化表示。

1.1  集合的定义

(1)     外延式定义

A={a,b,c} 。外延中的对象与概念的关系: a ∈ A

(2)     内涵式定义

A={x|x 满足所有内涵的条件 }

1.2  集合的关系

(1)     包含关系

集合 A 被集合 B 包含(A is included by B),或者说为 A 包含于 B指的是逻辑表达式 (∀x)( x ∈ A imp x ∈ B ) 成立的时候。也就是说A 的任何元素 x 都属于B ,写作 A ⊆ B

(2)     并集关系

当 A={x: P(x)} 和 B = {y: Q(y)} 为集合的时候,因 R(z) = P(z) or Q(z) 成为一个新的性质,于是就可以考虑成一个新的集合 C = {z: R(z)}。称其为,集合 A 和 B 的 并 或 并集(Union) ,写作 C = A U B。因为性质 P(x) 和 x ∈ A , Q(x) 和 x ∈ B 等价,所以A U B = {x: R(x)} = {x: P(x) or Q(x)} = {x: x ∈ A or x ∈ B} 成立。也就是说 A 和 B 的并集就是,属于A或B的所有元素的集合。

(3)     交集关系

跟定义并集一样对交集进行定义。 当A={x: P(x)} 和 B = {y: Q(y)}为集合的时候,因R(z) = P(z) and Q(z) 成为一个新的性质,于是就可以考虑成一个新的集合C = {z: R(z)}。称其为,集合 A 和B的 交 或 交集(Intersection),写作C = A ∩ B 。因为性质P(x) 和 x ∈ A , Q(x) 和x ∈ B 等价,所以 A ∩ B = {x: R(x)} = {x: P(x) and Q(x)} = {x: x ∈ A and x ∈ B} 成立。也就是说A 和 B 的交集就是 ,A 和 B 共有元素的集合。

(4)     补集

A = {x: P(x)}为集合的时候 ,{x: not P(x)} 叫做 A 的补集(Complement),写作 A'。这个集合是,不属于A的元素的集合。

 

2       JAVA语言中的数组

数组是一组具有相同类型和名称的变量的集合。这些变量称为数组的元素,每个数组元素都有一个编号,这个编号叫做下标,我们可以通过下标来区别这些元素。数组元素的个数有时也称之为数组的长度。一般情况下,数组的元素类型必须相同,可以是前面讲过的各种基本数据类型。但当数组类型被指定为变体型时,它的各个元素就可以是不同的类型。数组和变量一样,也是有作用域的,按作用域的不同可以把数组分为:过程级数组(或称为局部数组)、模块级数组以及全局数组。

2.1  数组的基本特性

数组是一种高效的存储和随机访问对象引用序列的方式,使用数组可以快速的访问数组中的元素。但 是当创建一个数组对象 ( 注意和对象数组的区别 ) 后,数组的大小也就固定了,当数组空间不足的时候就再创建一个新的数组,把旧的数组中所有的引用复制到新的数组中。

Java 中的数组和容器都需要进行边界检查,如果越界就会得到一个 RuntimeException 异常。这点和 C++ 中有所不同, C++ 中 vector 的操作符 [] 不会做边界检查,这在速度上会有一定的提高, Java 的数组和容器会因为时刻存在的边界检查带来一些性能上的开销。

Java 中通用的容器类不会以具体的类型来处理对象,容器中的对象都是以 Object 类型处理的,这是 Java 中所有类的基类。另外,数组可以保存基本类型,而容器不能,它只能保存任意的 Java 对象。

一般情况下,考虑到效率与类型检查,应该尽可能考虑使用数组。如果要解决一般化的问题,数组可能会受到一些限制,这时可以使用 Java 提供的容器类。

2.2  数组的相关操作

在 java .util.Arrays 类中,有许多 static 静态方法,提供了操作数组的一些基本功能:

 

    equals() 方法 ---- 用于比较两个数组是否相等,相等的条件是两个数组的元素个数必须相等,并且对应位置的元素也相等。

    fill() 方法 ---- 用以某个值填充整个数组,这个方法有点笨。

    asList() 方法 ---- 接受任意的数组为参数,将其转变为 List 容器。

    binarySearch() 方法 ---- 用于在已经排序的数组中查找元素,需要注意的是必须是已经排序过的数组。当 Arrays.binarySearch() 找到了查找目标时,该方法将返回一个等于或大于 0 的值,否则将返回一个负值,表示在该数组目前的排序状态下此目标元素所应该插入的位置。负值的计算公式是 “-x-1” 。 x 指的是第一个大于查找对象的元素在数组中的位置,如果数组中所有的元素都小于要查找的对象,则 x = a.size() 。如果数组中包含重复的元素,则无法保证找到的是哪一个元素,如果需要对没有重复元素的数组排序,可以使用 TreeSet 或者 LinkedHashSet 。另外,如果使用 Comparator 排序了某个对象数组,在使用该方法时必须提供同样的 Comparator 类型的参数。需要注意的是,基本类型数组无法使用 Comparator 进行排序。

   sort() 方法 ---- 对数组进行升序排序。

在 Java 标准类库中,另有 static 方法 System.arraycopy() 用来复制数组,它针对所有类型做了重载。

3       JAVA语言中的数组类

数组与其它容器的区别体现在三个方面:效率,类型识别以及可以持有primitives。数组是Java 提供的,能随机存储和访问reference序列的诸多方法中的,最高效的一种。数组是一个简单的线性序列,所有它可以快速的访问其中的元素。但是速度是 有代价的;当你创建了一个数组之后,它的容量就固定了,而且在其生命周期里不能改变。也许你会提议先创建一个数组,等到快不够用的时候,再创建一个新的, 然后将旧的数组里的reference全部导到新的里面。其实(我们以后会讲的)ArrayList就是这么做的。但是这种灵活性所带来的开销,使得 ArrayList的效率比起数组有了明显下降。Java 对数组和容器都做边界检查;如果过了界,它旧会给一个RuntimeException。这种异常表明这个错误是由程序员造成的,这样你就用不着再在程序 里面检查了。

    还有一些泛型容器类包括List,Set 和Map。他们处理对象的时候就好像这些对象都没有自己的具体类型一样。也就是说,容器将它所含的元素都看成是(Java 中所有类的根类)Object的。这样你只需要建一种容器,就能把所有类型的对象全都放进去。从这个角度来看,这种做法很不错(只是苦了 primitive。如果是常量,你还可以用Java 的 primitive的Wrapper类;如果是变量,那就只能放在你自己的类里了)。与其他泛型容器相比,这里体现数组的第二革优势:创建数组的时候,你 也同时指明了它所持有的对象的类型(这又引出了第三点--数组可以持有primitives,而容器却不行)。也就是说,它会在编译的时候作类型检查,从 而防止你插入错误类型的对象,或者是在提取对象的时候把对象的类型给搞错了。Java 在编译和运行时都能阻止你将一个不恰当的消息传给对象。所有这并不是说使用容器就有什么危险,只是如果编译器能够帮你指定,那么程序运行会更快,最终用户 也会较少收到程序运行异常的骚扰。

    从效率和类型检查的角度来看,使用数组总是没错的。但是,如果你在解决一个更为一般的问题,那数组就会显得功能太弱了点。

3.1  数组是第一流的对象

    不管你用的是那种类型的数组,数组的标识符实际上都是一个“创建在堆(heap)里的实实在在的对象的”reference。实际上是那个对象持 有其他对象的reference。你即可以用数组的初始化语句,隐含地创建这个对象,也可以用new表达式,明确地创建这个对象,只读的length属性 能告诉你数组能存储多少元素。它是数组对象的一部分(实际上也是你唯一能访问的属性或方法)。‘[]’语法是另一条访问数组对象的途径。

    你没法知道数组里面究竟放了多少元素,因为length只是告诉你数组能放多少元素,也就是说是数组对象的容量,而不是它真正已经持有的元素的数 量。但是,创建数组对象的时候,它所持有的reference都会被自动地初始化为null,所以你可以通过检查数组的某个 “槽位”是否为null,来判断它是否持有对象。以此类推,primitive的数组,会自动来数字初始化为零,字符初始化为 (char)0,boolean初始化为false。

3.2  Arrays类的应用

    java .util 里面有一个Arrays类,它包括了一组可用于数组的static方法,这些方法都是一些实用工具。其中有四个基本方法:用来比较两个数组是否相等的 equals();用来填充的fill();用来对数组进行排序的sort();以及用于在一个已排序的数组中查找元素的 binarySearch()。所有这些方法都对primitive和Object进行了重载。此外还有一个asList()方法,它接受一个数组,然后 把它转成一个List容器。

    虽然Arrays还是有用的,但它的功能并不完整。举例来说,如果它能让我们不用写for循环就能直接打印数组,那就好了。此外,正如你所看到的 fill()只能用一个值填数组。所以,如果你想把随即生成的数字填进数组的话,fill()是无能为力的。

3.2.1       复制一个数组

  Java 标准类库提供了一个System.arraycopy()的static方法。相比for循环,它能以更快的速度拷贝数组。 System.arraycopy()对所有类型都作了重载。

  对象数组和primitive数组都能拷贝。但是如果你拷贝的是对象数组,那么你只拷贝了它们的reference--对象本身不会被拷贝。这被 成为浅拷贝(shallow copy)。

3.2.2       数组的比较

    为了能比较数组是否完全相等,Arrays提供了经重载的equals()方法。当然,也是针对各种primitive以及 Object的。两个数组要想完全相等,他们必须有相同数量的元素,而且数组的每个元素必须与另一个数组的相对应的位置上的元素相等。元素的相等姓,用 equals()判断。(对于 primitive,它会使用其wrapper类的equals();比如int使用Integer.equals()。)。

3.2.3       数组元素的比较

    Java 里面有两种能让你实现比较功能的方法。一是实现java .lang.Comparable 接口,并以此实现类“自有的”比较方法。这是一个很简单的接口,它只有一个方法compareTo()。这个方法能接受另一个对象作为参数,如果现有对象 比参数小,它就会返回一个负数,如果相同则返回零,如果现有的对象比参数大,它就返回一个正数。 static randInt()方法会生成一个介于0到100之间的正数。

    现在架设,有人给你一个没有实现Comparable接口的类,或者这个类实现了Comparable接口,但是你发现它的工作方式不是你所希望 的,于是要重新定义一个新的比较方法。Java 没有强求你一定要把比较代码塞进类里,它的解决方案是使用“策略模式(strategy design pattern)”。有了策略之后,你就能把会变的代码封装到它自己的类里(即所谓的策略对象strategy object)。你把策略对象交给不会变的代码,然后用它运用策略完成整个算法。这样,你就可以用不同的策略对象来表示不同的比较方法,然后把它们都交给 同一个排序程序了。接下来就要“通过实现Comparator接口”来定义策略对象了。这个接口有两个方法compare()和equals()。但是除 非是有特殊的性能要求,否则你用不着去实现equals()。因为只要是类,它就都隐含地继承自Object,而Object里面已经有了一个 equals()了。所以你尽可以使用缺省的Object的equals(),这样就已经满足接口的要求了。

3.2.4       数组的排序

   有了内置的排序方法之后,你就能对任何数组排序了,不论是primitive的还是对象数组的,只要它实现了Comparable接口或有一个与 之相关的Comparator对象就行了。

   Java 标准类库所用的排序算法已经作了优化--对primitive,它用的是“快速排序(Quicksort)”,对对象,它用的是“稳定合并排序 (stable merge sort)”。所以除非是prolier表明排序算法是瓶颈,否则你不用为性能担心。

3.2.5       查询有序数组

   一旦数组排完序,你就能用Arrays.binarySearch()进行快速查询了。但是切忌对一个尚未排序的数组使用 binarySearch();因为这么做的结果是没意义的。    如果Arrays.binarySearch()找到了,它就返回一个大于或等于0的值。否则它就返回一个负值,而这个负值要表达的意思是,如果 你手动维护这个数组的话,这个值应该插在哪个位置。

 

4       JAVA语言中的集合

Java容器类类库的用途是“保存对象”,并将其划分为两个不同的概念:

1)  Collection 。 一组对立的元素,通常这些元素都服从某种规则。List必须保持元素特定的顺序,而Set 不能有重复元素。

2)  Map 。 一组 成对的“键值对”对象。初看起来这似乎应该是一个Collection ,其元素是成对的对象,但是这样的设计实现起来太笨拙了,于是我们将Map明确的提取出来形成一个独立的概念。另一方面,如果使用Collection 表示Map的部分内容,会便于查看此部分内容。因此Map一样容易扩展成多维Map ,无需增加新的概念,只要让Map中的键值对的每个“值”也是一个Map即可。

Collection和Map的区别在于容器中每个位置保存的元素个数。Collection 每个位置只能保存一个元素(对象)。此类容器包括:List ,它以特定的顺序保存一组元素;Set 则是元素不能重复。Map保存的是“键值对”,就像一个小型数据库。我们可以通过“键”找到该键对应的“值”。

u     Collection – 对象之间没有指定的顺序,允许重复元素。

u     Set –  对象之间没有指定的顺序,不允许重复元素

u     List–  对象之间有指定的顺序,允许重复元素,并引入位置下标。

u     Map – 接口用于保存关键字(Key)和数值(Value)的集合,集合中的每个对象加入时都提供数值和关键字。Map 接口既不继承 Set 也不继承 Collection。

4.1  Collection

Collection 接口用于表示任何对象或元素组。想要尽可能以常规方式处理一组元素时,就使用这一接口。Collection 在前面的大图也可以看出,它是List和Set 的父类。并且它本身也是一个接口。它定义了作为集合所应该拥有的一些方法。如下:(注意:集合必须只有对象,集合中的元素不能是基本数据类型。)

Collection接口支持如添加和除去等基本操作。设法除去一个元素时,如果这个元素存在,除去的仅仅是集合中此元素的一个实例。

u     boolean add(Object element)

u     boolean remove(Object element)

Collection 接口还支持查询操作:

u     int size()

u     boolean isEmpty()

u     boolean contains(Object element)

u     Iterator iterator()

组操作 :Collection 接口支持的其它操作,要么是作用于元素组的任务,要么是同时作用于整个集合的任务。

u     boolean containsAll(Collection collection)

u     boolean addAll(Collection collection)

u     void clear()

u     void removeAll(Collection collection)

u     void retainAll(Collection collection)

containsAll() 方法允许您查找当前集合是否包含了另一个集合的所有元素,即另一个集合是否是当前集合的子集。其余方法是可选的,因为特定的集合可能不支持集合更改。 addAll() 方法确保另一个集合中的所有元素都被添加到当前的集合中,通常称为 clear() 方法从当前集合中除去所有元素。 removeAll() 方法类似于 clear() ,但只除去了元素的一个子集。 retainAll() 方法类似于 removeAll() 方法,不过可能感到它所做的与前面正好相反:它从当前集合中除去不属于另一个集合的元素,即

4.2 List

List 就是列表的意思,它是Collection 的一种,继承了 Collection 接口,以定义一个允许重复项的有序集合。该接口不但能够对列表的一部分进行处理,还添加了面向位置的操作。List 是按对象的进入顺序进行保存对象,而不做排序或编辑操作。它除了拥有Collection接口的所有的方法外还拥有一些其他的方法。

面向位置的操作包括插入某个元素或 Collection 的功能,还包括获取、除去或更改元素的功能。在 List 中搜索元素可以从列表的头部或尾部开始,如果找到元素,还将报告元素所在的位置。

u       void add(int index, Object element) :添加对象element到位置index上

u       boolean addAll(int index, Collection collection) :在index位置后添加容器collection中所有的元素

u       Object get(int index) :取出下标为index的位置的元素

u       int indexOf(Object element) :查找对象element 在List中第一次出现的位置

u       int lastIndexOf(Object element) :查找对象element 在List中最后出现的位置

u       Object remove(int index) :删除index位置上的元素

u       Object set(int index, Object element) :将index位置上的对象替换为element 并返回老的元素。

4.3  Map

Map 接口不是 Collection 接口的继承。而是从自己的用于维护键-值关联的接口层次结构入手。按定义,该接口描述了从不重复的键到值的映射。我们可以把这个接口方法分成三组操作:改变、查询和提供可选视图。改变操作允许您从映射中添加和除去键-值对。键和值都可以为 null。但是,您不能把 Map 作为一个键或值添加给自身。

u       Object put(Object key,Object value):用来存放一个键-值对Map

u       Object remove(Object key):根据key(),移除一个键-值对,并将值返回

u       void putAll(Map mapping) :将另外一个Map中的元素存入当前的Map

u       void clear() :清空当前Map中的元素

查询操作允许您检查映射内容:

u       Object get(Object key) :根据key()取得对应的值

u       boolean containsKey(Object key) :判断Map中是否存在某键(key

u       boolean containsValue(Object value):判断Map中是否存在某值(value)

u       int size():返回Map键-值对的个数

u       boolean isEmpty() :判断当前Map是否为空

最后一组方法允许您把键或值的组作为集合来处理。

u       public Set keySet() :返回所有的键(key),并使用Set容器存放

u       public Collection values() :返回所有的值(Value),并使用Collection存放

u       public Set entrySet() 返回一个实现 Map.Entry 接口的元素 Set

因为映射中键的集合必须是唯一的,就使用 Set 来支持。因为映射中值的集合可能不唯一,就使用 Collection 来支持。最后一个方法返回一个实现 Map.Entry 接口的元素 Set。

4.4  Set

按照定义,Set 接口继承 Collection 接口,而且它不允许集合中存在重复项。所有原始方法都是现成的,没有引入新方法。具体的 Set 实现类依赖添加的对象的 equals() 方法来检查等同性。我们简单的描述一下各个方法的作用:

u     public int size() :返回set中元素的数目,如果set包含的元素数大于Integer.MAX_VALUE,返回Integer.MAX_VALUE

u     public boolean isEmpty() :如果set中不含元素,返回true

u     public boolean contains(Object o) :如果set包含指定元素,返回true

u     public Iterator iterator()

l         返回set中元素的迭代器

l         元素返回没有特定的顺序,除非set是提高了该保证的某些类的实例

u     public Object[] toArray() :返回包含set中所有元素的数组

u     public Object[] toArray(Object[] a) :返回包含set中所有元素的数组,返回数组的运行时类型是指定数组的运行时类型

u     public boolean add(Object o) :如果set中不存在指定元素,则向set加入

u     public boolean remove(Object o) :如果set中存在指定元素,则从set中删除

u     public boolean removeAll(Collection c) :如果set包含指定集合,则从set中删除指定集合的所有元素

u     public boolean containsAll(Collection c) :如果set包含指定集合的所有元素,返回true。如果指定集合也是一个set,只有是当前set的子集时,方法返回true

u     public boolean addAll(Collection c) :如果set中中不存在指定集合的元素,则向set中加入所有元素

u     public boolean retainAll(Collection c) :只保留set中所含的指定集合的元素(可选操作)。换言之,从set中删除所有指定集合不包含的元素。 如果指定集合也是一个set,那么该操作修改set的效果是使它的值为两个set的交集

u     public boolean removeAll(Collection c) :如果set包含指定集合,则从set中删除指定集合的所有元素

u     public void clear() :从set中删除所有元素

 

5       JAVA语言的数组和集合应用实例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章