精华内容
下载资源
问答
  • 实体-联系模型

    千次阅读 2020-12-20 22:08:32
    实体-联系(Entity-Relationship, ...(可从面向对象的类含义类似)实体集是相同类型即具有相同性质(或属性)的一个实体集合。在建模汇中,我们通常抽象地使用术语“实体集”,而不是指某个个别实体的特别集合。 实体

    实体-联系(Entity-Relationship, E-R)模型(以下简称E-R模型)的提出旨在方便数据库的设计,它是通过允许定义代表数据全局逻辑结构的企业模式实现的。
    E-R模型采用三个基本概念:实体集、联系集和属性。

    实体集

    实体(entity)是现实世界中可区别于所有其他对象的一个“事物”或“对象”。(与面向对象的类含义类似)实体集是相同类型即具有相同性质(或属性)的一个实体集合。在建模汇中,我们通常抽象地使用术语“实体集”,而不是指某个个别实体的特别集合。
    实体集不必互不相交。如可以定义大学里所有人的实体集(person)。一个person实体可以是teacher实体,也可以是student实体,可以既是teacher实体又是student实体,也可以都不是。
    实体通过一组属性(attribute)来表示。属性是实体集中每个成员所拥有的描述性性质。且每个属性都有一个值。

    弱实体集

    没有足够的属性以形成主码的实体集称为弱实体集(weak entity set)。有主码的实体集称作强实体集(strong entity set)。弱实体集必须与另一个称作标识(identitying)或属主实体集的实体集关联才能有意义。也就是说,弱实体集的存在依赖于标识实体集。将弱实体集与其标识实体集相连的联系称为标识性联系
    标识性联系是从弱实体集到标识实体集多对一的,并且弱实体集在联系中的参与是全部的。
    虽然弱实体集没有主码,但仍需要区分依赖于特定强实体集的弱实体集中实体的方法。弱实体集的分辨符是使得我们进行这种区分的属性集合。弱实体集的分辨符也称为该实体集的部分码
    弱实体的主码由标志实体集的主码加上该弱实体集的分辨符构成。

    联系集

    联系(relationship)是指多个实体间的相互关联。联系集是相同类型联系的集合。
    联系集也可以具有描述性属性(descriptive attribute)。如果teacher实体集与student实体集的联系集advisor。可以将属性date与该联系集联系起来,以表示教师成为学生的老师的日期。
    数据库中的大部分联系集都是二元的。然而,有时联系集会涉及多于两个实体集。参与联系集的实体集的数目称为联系集的度(degree)。二元联系集的度为2,三元联系集的度为3,以此类推。

    非二元的联系集

    对于非二元联系集,为了避免混淆,只允许在一个联系集外有一个箭头。(如果有多个箭头,则无法表明对应的哪个实体)。而函数依赖可以以一种不会混淆的方式描述实体间的联系。

    属性

    每个属性都有一个可取值的集合,称为该属性的域(domain),或者值集(value set)。 严格来说,实体集的属性是将实体集映射到域的函数。由于一个实体集可能有多个属性,因此每个实体可以用一组(属性,数据值)对来表示,实体集的每个属性对应一个这样的对。
    E-R模型中的属性可以按照如下的属性类型来划分:

    • 简单(simple)和复合(composite)属性。简单属性不能划分为更小的部分。复合属性可以再划分为更小的部分。 复合属性帮助我们把相关属性聚集起来,使模型更清晰。注意,复合属性可以是有层次的。如address可以包含street、city、state等,而street可以进一步分解为street_number、street_name、apartment_number。
    • 单值(single-valued)和多值(multi-valued)属性。一般情况下,一个属性对应一个值,这样的属性称为单值属性。如stuent_ID属性只对应于一个学生ID。而在某些情况下对某个特定实体而言,一个属性可能对应于一组值。以phone_number为例,每个教师可以有零个、一个或多个电话号码。这样的属性称为多值属性。为了表示一个多值属性,用花括号将属性名包住;如{phone_number}。
    • 派生(derived属性)。派生属性的值可以从别的相关属性或实体派生(计算)出来。如age属性表示年龄,如果还具有属性date_of_birth,就可以从当前的日期和date_of_birth计算出age。派生属性的值不存储,而是在需要时计算出来。
      当实体在某个属性上没有值时,使用空(null)值。空值可以表示“不适用”,即该实体的这个属性不存在值。空还可以用来表示属性值未知。未知的值可能是缺失的(值不存在),或不知道的(不知道该值是否确实存在)。

    删除冗余属性

    一个好的实体-联系设计不包含冗余的属性。但是在实际开发中,实现这一点需要极大的代价。

    约束

    可以定义一些数据库中的数据必须要满足的约束。

    映射基数(Mapping Cardinality)

    映射基数表示一个实体通过一个联系集能关联的实体的个数。对于实体集A和B之间的二元联系集R来说,映射基数必然是以下情况之一:
    一对一(one-to-one):A中的一个实体至多与B中的一个实体相关联,并且B中的一个实体也至多与A中的一个实体相关联。
    一对多(one-to-many):A中的一个实体可以与B中的任意数目(零个或多个)实体相关联,而B中的一个实体至多与A中的一个实体相关联。
    多对一(many-to-one):A中的一个实体至多与B中的一个实体相关联,而B中的一个实体可以与A中的任意数目(零个或多个)的实体相关联。
    多对多(many-to-many):A中的一个实体可以与B中的任意数目(零个或多个)实体相关联,,并且B中的一个实体也可以与A中的任意数目(零个或多个)的实体相关联。
    注意,考虑映射关系时,一定要同时考虑A->B和B->A两个方面,而不能只考虑其中一方面而忽略另一方面,从而导致错误的设计。

    参与约束

    如果实体集E中的每个实体都参与到联系集R的至少一个联系中,那么实体集E在联系集R中的参与称为全部的。如果实体集E中只有部分参与到联系集R中,那么实体集E在联系集R中的参与称为部分的。如我们期望每个student实体通过advisor联系同至少一名教师相联系,因此student在联系集advisor中的参与是全部的。相反地,一个teacher不是必须要指导一个学生。因此,很可能只有一部分teacher实体通过advisor联系同student相关联,于是teacher在advisor中的参与是部分的。

    我们必须有一个区分给定实体集中实体的方法。从概念上来说,各个实体是互异的;但从数据库的观点来看,它们的区别必须通过其属性表明。实体的码是一个足以区分每个实体属性集。关系模式中的超码、候选码、主码的概念同样适用于实体集。
    码同样可以唯一标识联系,并从而将联系相互区分开来。联系集的主码结构依赖于联系集的映射基数。如果联系集是多对多的,那么联系集的主码由两个实体集的主码的并集构成。如果联系是多对一的,那么多的实体的主码就是联系集的主码。如果联系集是一对一的,那么两个实体的任一主码就是联系集的主码。

    E-R数据模式转换为关系模式

    E-R模型和关系数据库模型都是现实世界企业抽象的逻辑表示。由于两种模型采用类似的设计原则,因此可将E-R设计转换为关系设计。

    具有简单属性的强实体集的表示

    设E是只具有简单描述性属性a1,a2,…,an的强实体集。我们用具有n个不同属性的模式E来表示这个实体集。对于从强实体集转换而来的模式,强实体集的主码就是生成的模式的主码。

    具有复杂属性的强实体集的表示

    当一个强实体集具有非简单属性时,可以通过为每个子属性创建一个单独的属性来处理复合属性,而不为复合属性自身创建一个单独的属性。
    多值属性的处理不同于其他属性。对于一个多值属性M,构建关系模式R,该模式包含一个对应于M的属性A,以及对应于M所在的实体集或联系集的主码的属性。另外,在多值属性构建的关系模式上建立外码约束,由实体集的主码所生成的属性去参照实体集所生成的关系。
    派生属性并不在关系数据模型中显式地表达出来。

    弱实体集的表示

    设A是具有属性a1,a2,…,an的弱实体集,设B是A所依赖的强实体集,设B的主码包括b1,b2,…,bn。
    对于从弱实体集转换而来的模式,该模式的主码由其所依赖的强实体集的主码与弱实体集的分辨符组合而成。除了创建主码之外,还要在关系A上建立外码约束,该约束指明属性b1,b2,…,bn参照关系B的主码。外码约束保证表示弱实体的每个元组都有一个表示相应强实体的元组与之对应。

    联系集的表示

    设R是联系集,设a1,a2,…,an表示所有参与R的实体集的主码的并集构成的属性集合,设R的描述性属性(如果有)为b1,b2,…,bn。映射基数不同,主码的选择方式不同:

    • 对于多对多的二元联系集,参与实体集的主码属性的并集成为主码。
    • 对于一对多的二元联系集,任何一个实体集的主码都可以选作主码。这个选择是任意的。
    • 对于多对一或一对多的二元联系集,联系集中多的那一方的实体集的主码构成主码。
    • 对于边上没有箭头的n元联系集,所有参与实体集的主码属性的并集构成主码。
    • 对于边上有一个箭头的n元联系集,不在"箭头"侧的实体集的主码属性为模式的主码。
      此外,还需在关系模式R上建立外键约束。

    模式的冗余

    连接弱实体集和相应强实体集的联系集比较特殊。弱实体集的主码包含强实体集的主码。连接弱实体集与其所依赖强实体集的联系集的模式是冗余的,而且在基于E-R图的关系数据库设计不必给出。

    模式的合并

    在一对一的联系的情况下,联系集的关系模式可以跟参与联系的任何一个实体集的模式进行合并。即使参与是部分的,也可以通过空值来进行模式的合并。
    最后,还需考虑表示联系集的模式上本应有的外码约束。参照每一个参与联系集的实体集的外码约束本应存在。我们舍弃了参照联系集模式所合并入的实体集模式的约束,然后将另一个外码约束加到合并的模式中。

    实体-联系设计问题

    在实体-联系数据库模式中涉及到一些基本问题。

    用实体集还是用属性

    什么构成属性?什么构成实体集?对这两个问题并不能简单地回答。区分它们主要依赖于被建模的现实世界的企业结构,以及被讨论的属性的相关语义。
    一个常见的错误是用一个实体集的主码作为另一个实体集的属性,而不是用联系。例如,即使每名教师指指导一名学生,将student的ID作为teacher的属性也是不正确的。用advisor联系代表学生和教师之间的关联才是正确的方法,因为这样可以明确表示出两者之间的关系而不是将这种关系隐含在属性中。
    另一个常见的错误是将相关实体集主码属性为联系集的属性。这种做法是不对的,因为在联系集中已隐含这些主码属性。(这些属性默认已经在联系集中,不应再明确表示出来)

    用实体集还是联系集

    一个对象最好被表述为实体集还是联系集并不总是显而易见。在决定用实体集还是联系集可采用一个原则是,当描述发生在实体间的行为时采用联系集。这一方法在决定是否将某些属性表示为联系可能更适合时也很有用。

    二元还是n元联系集

    数据库中的联系通常都是二元的。一些看来非二元的联系实际上可以用多个二元联系更好地表示。事实上,一个非二元的(n元,n>2)联系集总可以用一组不同的二元联系集来替代。可以将这一过程直接推广到n元联系集的情况。因此在概念上可以限制E-R模型只包含二元联系集。然而,这种限制并不总是令人满意的。

    • 对于为表示联系集而创建的实体集,可能不得不为其创建一个标识属性。该标识属性和额外所需的那些联系集增加了设计的复杂度以及对总的存储空间的需求。
    • n元联系集可以更清晰地表示几个实体集参与单个联系集。
    • 有可能无法将三元联系上的约束转变为二元联系上的约束。例如,考虑一个约束,表明R是从A、B到C多对一的;也就是,来自A和B的每一对实体最多与一个C实体关联。这种约束就不能用联系集Ra、Rb和Rc上的基数约束表示。

    联系集中属性的布局

    一个联系的映射基数比率会影响联系集中属性的布局。因此,一对一或一对多联系集的属性可以放到一个参与该联系的实体集中,而不是放到联系集中。一对多联系集的属性仅可以重置到参与联系的“多”方的实体集中。而对于一对一的联系集,联系的属性可以放到任意一个参与联系的实体中。
    设计时将描述性属性作为联系集的属性还是实体集的属性这一决定反映出被建模企业的特点。
    属性位置的选择在多对多的联系集中体现得更清楚。同名的属性,放在实体集中还是联系集中其作用不同。

    扩展的E-R特性

    虽然基本的E-R概念足以对大多数数据库特征建模,但数据库的某些方面可以通过基本E-R模型作某些扩展来更恰当地表达。

    特化(Specialization)

    在实体集内部进行分组的过程称为特化。一个实体集可以根据多个可区分的特征进行特化。在E-R图中,特化用从特化实体指向另一个实体的空心箭头来表示。所以,这种关系也称为ISA关系。特化关系还可能形成超类-子类(superclass-subclass)联系。

    概化(Generalization)

    实体的共性可以通过概化来表达,概化是高层实体集与一个或多个低层实体集间的包含关系。对于所有实际应用来说,概化只不过是特化的逆过程。为企业设计E-R模型时,将配合使用这两个过程。

    聚集(Aggregation)

    聚集是一种抽象,通过这种抽象,联系被视为高层实体。
    当把聚集像其他实体集一样看待时,之前用于在联系集上创建主码和外码约束的规则,也同样可以应用于与聚集相关联的联系集。聚集的主码是定义该聚集的联系集的主码。不需要单独的关系来表示聚集;而使用从定义该聚集的联系创建出来的关系即可。

    数据库设计的其他方面

    数据约束和关系数据库设计

    使用SQL可以表达多种数据约束,包括主码约束、外码约束、check约束、断言和触发器。约束有多种目的。最明显的一个目的是自动的一致性保持。通过在SQL数据定义语言中表达约束,设计者能够确保数据库系统自己执行这些约束(显式声明约束)。
    显式声明约束的另一个优点是一些约束在数据库模式的设计中特别有用。
    数据约束在确定数据的物理结构时同样有用,可以将彼此紧密相关的数据存储在磁盘上邻近的区域,以便在磁盘访问时提高效率。如将索引建立在主码上,索引结构工作得更好。
    每次数据库更新时,执行约束会在性能上带来潜在的高代价。对于每次更新,系统都必须检查所有的约束,然后要么拒绝与约束冲突的更新,要么运行相应的触发器。性能损失的严重性,不仅仅取决于更新的频率,而且依赖于数据库的设计方式。

    使用需求:查询、性能

    数据库系统的性能时绝大多数企业信息系统的一个关键因素。性能不仅与计算能力的有效利用以及所使用的存储硬件有关,而且受到与系统交互的人的效率以及依赖数据库数据的处理的效率的影响。以下是效率的两个主要度量方法:

    • 吞吐量(throughput)————每单位时间里能够处理的查询或更新(通常指事务)的平均数量。
    • 响应时间(response time)————单个事务从开始到结束所需的平均时间或最长时间。

    授权需求

    授权约束同样会影响数据库的设计,因为SQL允许在数据库逻辑设计组件的基础上将访问权限授予用户。(现有主流数据库系统均已合理实现授权(基于角色分配))

    数据流、工作流

    术语工作流表示一个流程中的数据和任务的组合。当工作流在用户间移动以及用户执行他们在工作流中的任务时,工作流会与数据库系统交互。

    数据库设计的其他问题

    数据库设计通常不是一个一蹴而就的工作。一个组织的需求不断发展,它所需要存储的数据也会相应地发展。但是,对于一个已明确的需求,还是可以给出稳定的设计的。
    一个好的设计应该不止考虑当前的规定,还应该避免或者最小化由预计或有可能发生的改变而带来的改动。(需要做向上兼容的思考)
    最后,数据库设计在两个意义上是面向人的工作:系统的最终用户是人(使用该程序的用户);数据库设计者需与应用领域的专家进行广泛交互以理解应用的数据需求。所有涉及数据的人都有需要和偏好,为了数据库设计和部署在企业中获得成功,这些都是需要考虑的。

    参考

    数据库系统概念(第六版) A. Silberschatz H. F. Korth S. Sudarshan著 杨冬青 等译 第七章

    展开全文
  • 具有相同属性的实体具有由相同的特征和性质,用实体名及其属性名集合来抽象和刻画同类实体,称为实体型。 (3)实体集 同型实体集合称为实体集, (4)实体之间的联系 通常是指不同实体型的实体...

    (1)实体

    客观存在并可以相互区分的事物叫实体。

    (2)实体型

    具有相同属性的实体具有由相同的特征和性质,用实体名及其属性名集合来抽象和刻画同类实体,称为实体型。

    (3)实体集

    同型实体的集合称为实体集,

    (4)实体之间的联系

    通常是指不同实体型的实体集之间的联系,实体之间的联系有一对一,一对多,多对多等多种类型

    展开全文
  • Java泛型与集合类 在前面我们学习了最重要的类和对象,了解了面向对象编程的思想,注意,非常重要,面向对象是必须要深入理解和掌握的内容,不能草草结束。在本章节,我们会继续深入了解,从我们的泛型开始,再到...

    Java泛型与集合类

    在前面我们学习了最重要的类和对象,了解了面向对象编程的思想,注意,非常重要,面向对象是必须要深入理解和掌握的内容,不能草草结束。在本章节,我们会继续深入了解,从我们的泛型开始,再到我们的数据结构,最后再开始我们的集合类学习。

    走进泛型

    为了统计学生成绩,要求设计一个Score对象,包括课程名称、课程号、课程成绩,但是成绩分为两种,一种是以优秀、良好、合格 来作为结果,还有一种就是 60.0、75.5、92.5 这样的数字分数,那么现在该如何去设计这样的一个Score类呢?现在的问题就是,成绩可能是String类型,也可能是Integer类型,如何才能很好的去存可能出现的两种类型呢?

    public class Score {    String name;    String id;    Object score;  //因为Object是所有类型的父类,因此既可以存放Integer也能存放String  	public Score(String name, String id, Object score) {        this.name = name;        this.id = id;        this.score = score;    }}
    

    以上的方法虽然很好地解决了多种类型存储问题,但是Object类型在编译阶段并不具有良好的类型判断能力,很容易出现以下的情况:

    public static void main(String[] args) {    Score score = new Score("数据结构与算法基础", "EP074512", "优秀");  //是String类型的    //....    Integer number = (Integer) score.score;  //获取成绩需要进行强制类型转换,虽然并不是一开始的类型,但是编译不会报错}//运行时出现异常!Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer	at com.test.Main.main(Main.java:14)
    

    使用Object类型作为引用,取值只能进行强制类型转换,显然无法在编译期确定类型是否安全,项目中代码量非常之大,进行类型比较又会导致额外的开销和增加代码量,如果不经比较就很容易出现类型转换异常,代码的健壮性有所欠缺!(此方法虽然可行,但并不是最好的方法)

    为了解决以上问题,JDK1.5新增了泛型,它能够在编译阶段就检查类型安全,大大提升开发效率。

    public class Score<T> {   //将Score转变为泛型类<T>    String name;    String id;    T score;  //T为泛型,根据用户提供的类型自动变成对应类型    public Score(String name, String id, T score) {   //提供的score类型即为T代表的类型        this.name = name;        this.id = id;        this.score = score;    }}
    
    public static void main(String[] args) {    //直接确定Score的类型是字符串类型的成绩    Score<String> score = new Score<String>("数据结构与算法基础", "EP074512", "优秀");    Integer i = score.score;  //编译不通过,因为成员变量score类型被定为String!}
    

    泛型将数据类型的确定控制在了编译阶段,在编写代码的时候就能明确泛型的类型!如果类型不符合,将无法通过编译!

    泛型本质上也是一个语法糖(并不是JVM所支持的语法,编译后会转成编译器支持的语法,比如之前的foreach就是),在编译后会被擦除,变回上面的Object类型调用,但是类型转换由编译器帮我们完成,而不是我们自己进行转换(安全)

    //反编译后的代码public static void main(String[] args) {        Score score = new Score("数据结构与算法基础", "EP074512", "优秀");        String i = (String)score.score;   //其实依然会变为强制类型转换,但是这是由编译器帮我们完成的    }
    

    像这样在编译后泛型的内容消失转变为Object的情况称为类型擦除(重要,需要完全理解),所以泛型只是为了方便我们在编译阶段确定类型的一种语法而已,并不是JVM所支持的。

    综上,泛型其实就是一种类型参数,用于指定类型。

    泛型的使用

    泛型类

    上一节我们已经提到泛型类的定义,实际上就是普通的类多了一个类型参数,也就是在使用时需要指定具体的泛型类型。泛型的名称一般取单个大写字母,比如T代表Type,也就是类型的英文单词首字母,当然也可以添加数字和其他的字符。

    public class Score<T> {   //将Score转变为泛型类<T>    String name;    String id;    T score;  //T为泛型,根据用户提供的类型自动变成对应类型    public Score(String name, String id, T score) {   //提供的score类型即为T代表的类型        this.name = name;        this.id = id;        this.score = score;    }}
    

    在一个普通类型中定义泛型,泛型T称为参数化类型,在定义泛型类的引用时,需要明确指出类型:

     Score<String> score = new Score<String>("数据结构与算法基础", "EP074512", "优秀");
    

    此时类中的泛型T已经被替换为String了,在我们获取此对象的泛型属性时,编译器会直接告诉我们类型:

    Integer i = score.score;   //编译不通过,因为成员变量score明确为String类型
    

    注意,泛型只能用于对象属性,也就是非静态的成员变量才能使用:

    static T score;   //错误,不能在静态成员上定义
    

    由此可见,泛型是只有在创建对象后编译器才能明确泛型类型,而静态类型是类所具有的属性,不足以使得编译器完成类型推断。

    泛型无法使用基本类型,如果需要基本类型,只能使用基本类型的包装类进行替换!

    Score<double> score = new Score<double>("数据结构与算法基础", "EP074512", 90.5);  //编译不通过
    

    那么为什么泛型无法使用基本类型呢?回想上一节提到的类型擦除,其实就很好理解了。由于JVM没有泛型概念,因此泛型最后还是会被编译器编译为Object,并采用强制类型转换的形式进行类型匹配,而我们的基本数据类型和引用类型之间无法进行类型转换,所以只能使用基本类型的包装类来处理。

    类的泛型方法

    泛型方法的使用也很简单,我们只需要把它当做一个未知的类型来使用即可:

    public T getScore() {    //若方法的返回值类型为泛型,那么编译器会自动进行推断  return score;}public void setScore(T score) {   //若方法的形式参数为泛型,那么实参只能是定义时的类型  this.score = score;}
    
    Score<String> score = new Score<String>("数据结构与算法基础", "EP074512", "优秀");score.setScore(10);   //编译不通过,因为只接受String类型
    

    同样地,静态方法无法直接使用类定义的泛型(注意是无法直接使用,静态方法可以使用泛型)

    自定义泛型方法

    那么如果我想在静态方法中使用泛型呢?首先我们要明确之前为什么无法使用泛型,因为之前我们的泛型定义是在类上的,只有明确具体的类型才能开始使用,也就是创建对象时完成类型确定,但是静态方法不需要依附于对象,那么只能在使用时再来确定了,所以静态方法可以使用泛型,但是需要单独定义:

    public static <E> void test(E e){   //在方法定义前声明泛型  System.out.println(e);}
    

    同理,成员方法也能自行定义泛型,在实际使用时再进行类型确定:

    public <E> void test(E e){  System.out.println(e);}
    

    其实,无论是泛型类还是泛型方法,再使用时一定要能够进行类型推断,明确类型才行。

    注意一定要区分类定义的泛型和方法前定义的泛型!

    泛型引用

    可以看到我们在定义一个泛型类的引用时,需要在后面指出此类型:

    Score<Integer> score;  //声明泛型为Integer类型
    

    如果不希望指定类型,或是希望此引用类型可以引用任意泛型的Score类对象,可以使用?通配符,来表示自动匹配任意的可用类型:

    Score<?> score;   //score可以引用任意的Score类型对象了!
    

    那么使用通配符之后,得到的泛型成员变量会是什么类型呢?

    Object o = score.getScore();   //只能变为Object
    

    因为使用了通配符,编译器就无法进行类型推断,所以只能使用原始类型。

    在学习了泛型的界限后,我们还会继续了解通配符的使用。

    泛型的界限

    现在有一个新的需求,现在没有String类型的成绩了,但是成绩依然可能是整数,也可能是小数,这时我们不希望用户将泛型指定为除数字类型外的其他类型,我们就需要使用到泛型的上界定义:

    public class Score<T extends Number> {   //设定泛型上界,必须是Number的子类    private final String name;    private final String id;    private T score;    public Score(String name, String id, T score) {        this.name = name;        this.id = id;        this.score = score;    }    public T getScore() {        return score;    }}
    

    通过extends关键字进行上界限定,只有指定类型或指定类型的子类才能作为类型参数。

    同样的,泛型通配符也支持泛型的界限:

    Score<? extends Number> score;  //限定为匹配Number及其子类的类型
    

    同理,既然泛型有上限,那么也有下限:

    Score<? super Integer> score;   //限定为匹配Integer及其父类
    

    通过super关键字进行下界限定,只有指定类型或指定类型的父类才能作为类型参数。

    图解如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pcCn8ldJ-1634722435392)(http://images4.10qianwan.com/10qianwan/20191209/b_0_201912091523263309.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GcBCsGMT-1634722435394)(http://images4.10qianwan.com/10qianwan/20191209/b_0_201912091523264595.jpg)]

    那么限定了上界后,我们再来使用这个对象的泛型成员,会变成什么类型呢?

    Score<? extends Number> score = new Score<>("数据结构与算法基础", "EP074512", 10);Number o = score.getScore();    //得到的结果为上界类型
    

    也就是说,一旦我们指定了上界后,编译器就将范围从原始类型Object提升到我们指定的上界Number,但是依然无法明确具体类型。思考:那如果定义下限呢?

    那么既然我们可以给泛型类限定上界,现在我们来看编译后结果呢:

    //使用javap -l 进行反编译public class com.test.Score<T extends java.lang.Number> {  public com.test.Score(java.lang.String, java.lang.String, T);    LineNumberTable:      line 8: 0      line 9: 4      line 10: 9      line 11: 14      line 12: 19    LocalVariableTable:      Start  Length  Slot  Name   Signature          0      20     0  this   Lcom/test/Score;          0      20     1  name   Ljava/lang/String;          0      20     2    id   Ljava/lang/String;          0      20     3 score   Ljava/lang/Number;   //可以看到score的类型直接被编译为Number类  public T getScore();    LineNumberTable:      line 15: 0    LocalVariableTable:      Start  Length  Slot  Name   Signature          0       5     0  this   Lcom/test/Score;}
    

    因此,一旦确立上限后,编译器会自动将类型提升到上限类型。

    钻石运算符

    我们发现,每次创建泛型对象都需要在前后都标明类型,但是实际上后面的类型声明是可以去掉的,因为我们在传入参数时或定义泛型类的引用时,就已经明确了类型,因此JDK1.7提供了钻石运算符来简化代码:

    Score<Integer> score = new Score<Integer>("数据结构与算法基础", "EP074512", 10);  //1.7之前Score<Integer> score = new Score<>("数据结构与算法基础", "EP074512", 10);  //1.7之后
    

    泛型与多态

    泛型不仅仅可以可以定义在类上,同时也能定义在接口上:

    public interface ScoreInterface<T> {    T getScore();    void setScore(T t);}
    

    当实现此接口时,我们可以选择在实现类明确泛型类型或是继续使用此泛型,让具体创建的对象来确定类型。

    public class Score<T> implements ScoreInterface<T>{   //将Score转变为泛型类<T>    private final String name;    private final String id;    private T score;    public Score(String name, String id, T score) {         this.name = name;        this.id = id;        this.score = score;    }    public T getScore() {        return score;    }    @Override    public void setScore(T score) {        this.score = score;    }}
    
    public class StringScore implements ScoreInterface<String>{   //在实现时明确类型    @Override    public String getScore() {        return null;    }    @Override    public void setScore(String s) {    }}
    

    抽象类同理,这里就不多做演示了。

    多态类型擦除

    思考一个问题,既然继承后明确了泛型类型,那么为什么@Override不会出现错误呢,重写的条件是需要和父类的返回值类型、形式参数一致,而泛型默认的原始类型是Object类型,子类明确后变为Number类型,这显然不满足重写的条件,但是为什么依然能编译通过呢?

    class A<T>{    private T t;    public T get(){        return t;    }    public void set(T t){        this.t=t;    }}class B extends A<Number>{    private Number n;    @Override    public Number get(){   //这并不满足重写的要求,因为只能重写父类同样返回值和参数的方法,但是这样却能够通过编译!        return t;    }    @Override    public void set(Number t){        this.t=t;    }}
    

    通过反编译进行观察,实际上是编译器帮助我们生成了两个桥接方法用于支持重写:

    @Overridepublic Object get(){  return this.get();//调用返回Number的那个方法}@Overridepublic void set(Object t ){  this.set((Number)t ); //调用参数是Number的那个方法}
    

    数据结构基础

    警告!本章最难的部分!

    学习集合类之前,我们还有最关键的内容需要学习,同第一章一样,自底向上才是最佳的学习方向,比起直接带大家认识集合类,不如先了解一下数据结构,只有了解了数据结构基础,才能更好地学习集合类,同时,数据结构也是你以后深入学习JDK源码的必备条件!(学习不要快餐式!)当然,我们主要是讲解Java,数据结构作为铺垫作用,所以我们只会讲解关键的部分,其他部分可以下去自行了解。

    在计算机科学中,数据结构是一种数据组织、管理和存储的格式,它可以帮助我们实现对数据高效的访问和修改。更准确地说,数据结构是数据值的集合,可以体现数据值之间的关系,以及可以对数据进行应用的函数或操作。

    通俗地说,我们需要去学习在计算机中如何去更好地管理我们的数据,才能让我们对我们的数据控制更加灵活!

    线性表

    线性表是最基本的一种数据结构,它是表示一组相同类型数据的有限序列,你可以把它与数组进行参考,但是它并不是数组,线性表是一种表结构,它能够支持数据的插入、删除、更新、查询等,同时数组可以随意存放在数组中任意位置,而线性表只能依次有序排列,不能出现空隙,因此,我们需要进一步的设计。

    顺序表

    将数据依次存储在连续的整块物理空间中,这种存储结构称为顺序存储结构,而以这种方式实现的线性表,我们称为顺序表

    同样的,表中的每一个个体都被称为元素,元素左边的元素(上一个元素),称为前驱,同理,右边的元素(后一个元素)称为后驱

    img

    我们设计线性表的目标就是为了去更好地管理我们的数据,也就是说,我们可以基于数组,来进行封装,实现增删改查!既然要存储一组数据,那么很容易联想到我们之前学过的数组,数组就能够容纳一组同类型的数据。

    目标:以数组为底层,编写以下抽象类的具体实现

    /** * 线性表抽象类 * @param <E> 存储的元素(Element)类型 */public abstract class AbstractList<E> {    /**     * 获取表的长度     * @return 顺序表的长度     */    public abstract int size();    /**     * 添加一个元素     * @param e 元素     * @param index 要添加的位置(索引)     */    public abstract void add(E e, int index);    /**     * 移除指定位置的元素     * @param index 位置     * @return 移除的元素     */    public abstract E remove(int index);    /**     * 获取指定位置的元素     * @param index 位置     * @return 元素     */    public abstract E get(int index);}
    

    链表

    数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构

    实际上,就是每一个结点存放一个元素和一个指向下一个结点的引用(C语言里面是指针,Java中就是对象的引用,代表下一个结点对象)

    img

    利用这种思想,我们再来尝试实现上面的抽象类,从实际的代码中感受!

    比较:顺序表和链表的优异?

    顺序表优缺点:

    • 访问速度快,随机访问性能高
    • 插入和删除的效率低下,极端情况下需要变更整个表
    • 不易扩充,需要复制并重新创建数组

    链表优缺点:

    • 插入和删除效率高,只需要改变连接点的指向即可
    • 动态扩充容量,无需担心容量问题
    • 访问元素需要依次寻找,随机访问元素效率低下

    链表只能指向后面,能不能指向前面呢?双向链表!


    栈和队列实际上就是对线性表加以约束的一种数据结构,如果前面的线性表的掌握已经ok,那么栈和队列就非常轻松了!

    栈遵循先入后出原则,只能在线性表的一端添加和删除元素。我们可以把栈看做一个杯子,杯子只有一个口进出,最低处的元素只能等到上面的元素离开杯子后,才能离开。

    img

    向栈中插入一个元素时,称为入栈(压栈),移除栈顶元素称为出栈,我们需要尝试实现以下抽象类型:

    /** * 抽象类型栈,待实现 * @param <E> 元素类型 */public abstract class AbstractStack<E> {    /**     * 出栈操作     * @return 栈顶元素     */    public abstract E pop();    /**     * 入栈操作     * @param e 元素     */    public abstract void push(E e);}
    

    其实,我们的JVM在处理方法调用时,也是一个栈操作:

    img

    所以说,如果玩不好递归,就会像这样:

    public class Main {    public static void main(String[] args) {        go();    }    private static void go(){        go();    }}Exception in thread "main" java.lang.StackOverflowError	at com.test.Main.go(Main.java:13)	at com.test.Main.go(Main.java:13)	at com.test.Main.go(Main.java:13)	at com.test.Main.go(Main.java:13)	at com.test.Main.go(Main.java:13)	at com.test.Main.go(Main.java:13)	at com.test.Main.go(Main.java:13)	at com.test.Main.go(Main.java:13)  ...
    

    栈的深度是有限制的,如果达到限制,将会出现StackOverflowError错误(注意是错误!说明是JVM出现了问题)

    队列

    队列同样也是受限制的线性表,不过队列就像我们排队一样,只能从队尾开始排,从队首出。

    img

    所以我们要实现以下内容:

    /** * * @param <E> */public abstract class AbstractQueue<E> {    /**     * 进队操作     * @param e 元素     */    public abstract void offer(E e);    /**     * 出队操作     * @return 元素     */    public abstract E poll();}
    

    二叉树

    本版块主要学习的是二叉树,树也是一种数据结构,但是它使用起来更加的复杂。

    我们前面已经学习过链表了,我们知道链表是单个结点之间相连,也就是一种一对一的关系,而树则是一个结点连接多个结点,也就是一对多的关系。

    img

    一个结点可以有N个子结点,就像上图一样,看起来就像是一棵树。而位于最顶端的结点(没有父结点)我们称为根结点,而结点拥有的子节点数量称为,每向下一级称为一个层次,树中出现的最大层次称为树的深度(高度)

    二叉树

    二叉树是一种特殊的树,每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点,位于两边的子结点称为左右子树(注意,左右子树是明确区分的,是左就是左,是右就是右)

    img

    数学性质:

    • 在二叉树的第i层上最多有2^(i-1) 个节点。
    • 二叉树中如果深度为k,那么最多有2^k-1个节点。

    设计一个二叉树结点类:

    public class TreeNode<E> {    public E e;   //当前结点数据    public TreeNode<E> left;   //左子树    public TreeNode<E> right;   //右子树}
    

    二叉树的遍历

    顺序表的遍历其实就是依次有序去访问表中每一个元素,而像二叉树这样的复杂结构,我们有四种遍历方式,他们是:前序遍历、中序遍历、后序遍历以及层序遍历,本版块我们主要讨论前三种遍历方式:

    • 前序遍历:从二叉树的根结点出发,到达结点时就直接输出结点数据,按照先向左在向右的方向访问。ABCDEF
    • 中序遍历:从二叉树的根结点出发,优先输出左子树的节点的数据,再输出当前节点本身,最后才是右子树。CBDAEF
    • 后序遍历:从二叉树的根结点出发,优先遍历其左子树,再遍历右子树,最后在输出当前节点本身。CDBFEA

    满二叉树和完全二叉树

    满二叉树和完全二叉树其实就是特殊情况下的二叉树,满二叉树左右的所有叶子节点都在同一层,也就是说,完全把每一个层级都给加满了结点。完全二叉树与满二叉树不同的地方在于,它的最下层叶子节点可以不满,但是最下层的叶子节点必须靠左排布。

    img

    其实满二叉树和完全二叉树就是有一定规律的二叉树,很容易理解。

    快速查找

    我们之前提到的这些数据结构,很好地帮我们管理了数据,但是,如果需要查找某一个元素是否存在于数据结构中,如何才能更加高效的去完成呢?

    哈希表

    通过前面的学习,我们发现,顺序表虽然查询效率高,但是插入删除有严重表更新的问题,而链表虽然弥补了更新问题,但是查询效率实在是太低了,能否有一种折中方案?哈希表!

    不知大家在之前的学习中是否发现,我们的Object类中,定义了一个叫做hashcode()的方法?而这个方法呢,就是为了更好地支持哈希表的实现。hashcode()默认得到的是对象的内存地址,也就是说,每个对象的hashCode都不一样。

    哈希表,其实本质上就是一个存放链表的数组,那么它是如何去存储数据的呢?我们先来看看长啥样:

    img

    数组中每一个元素都是一个头结点,用于保存数据,那我们怎么确定数据应该放在哪一个位置呢?通过hash算法,我们能够瞬间得到元素应该放置的位置。

    //假设hash表长度为16,hash算法为:private int hash(int hashcode){  return hashcode % 16;}
    

    设想这样一个问题,如果计算出来的hash值和之前已经存在的元素相同了呢?这种情况我们称为hash碰撞,这也是为什么要将每一个表元素设置为一个链表的头结点的原因,一旦发现重复,我们可以往后继续添加节点。

    当然,以上的hash表结构只是一种设计方案,在面对大额数据时,是不够用的,在JDK1.8中,集合类使用的是数组+二叉树的形式解决的(这里的二叉树是经过加强的二叉树,不是前面讲得简单二叉树,我们下一节就会开始讲)

    二叉排序树

    我们前面学习的二叉树效率是不够的,我们需要的是一种效率更高的二叉树,因此,基于二叉树的改进,提出了二叉查找树,可以看到结构像下面这样:

    img

    不难发现,每个节点的左子树,一定小于当前节点的值,每个节点的右子树,一定大于当前节点的值,这样的二叉树称为二叉排序树。利用二分搜索的思想,我们就可以快速查找某个节点!

    平衡二叉树

    在了解了二叉查找树之后,我们发现,如果根节点为10,现在加入到结点的值从9开始,依次减小到1,那么这个表就会很奇怪,就像下面这样:

    img

    显然,当所有的结点都排列到一边,这种情况下,查找效率会直接退化为最原始的二叉树!因此我们需要维持二叉树的平衡,才能维持原有的查找效率。

    现在我们对二叉排序树加以约束,要求每个结点的左右两个子树的高度差的绝对值不超过1,这样的二叉树称为平衡二叉树,同时要求每个结点的左右子树都是平衡二叉树,这样,就不会因为一边的疯狂增加导致失衡。我们来看看以下几种情况:

    img

    左左失衡

    img

    右右失衡

    img

    左右失衡

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwEldtSl-1634722435400)(https://pic002.cnblogs.com/images/2012/214741/2012072219540371.png)]

    右左失衡

    通过以上四种情况的处理,最终得到维护平衡二叉树的算法。

    红黑树

    红黑树也是二叉排序树的一种改进,同平衡二叉树一样,红黑树也是一种维护平衡的二叉排序树,但是没有平衡二叉树那样严格(平衡二叉树每次插入新结点时,可能会出现大量的旋转,而红黑树保证不超过三次),红黑树降低了对于旋转的要求,因此效率有一定的提升同时实现起来也更加简单。但是红黑树的效率却高于平衡二叉树,红黑树也是JDK1.8中使用的数据结构!

    img

    红黑树的特性:
    (1)每个节点或者是黑色,或者是红色。
    (2)根节点是黑色。
    (3)每个叶子节点的两边也需要表示(虽然没有,但是null也需要表示出来)是黑色。
    (4)如果一个节点是红色的,则它的子节点必须是黑色的。
    (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

    我们来看看一个节点,是如何插入到红黑树中的:

    基本的 插入规则和平衡二叉树一样,但是在插入后:

    1. 将新插入的节点标记为红色
    2. 如果 X 是根结点(root),则标记为黑色
    3. 如果 X 的 parent 不是黑色,同时 X 也不是 root:
    • 3.1 如果 X 的 uncle (叔叔) 是红色

      • 3.1.1 将 parent 和 uncle 标记为黑色
      • 3.1.2 将 grand parent (祖父) 标记为红色
      • 3.1.3 让 X 节点的颜色与 X 祖父的颜色相同,然后重复步骤 2、3
    • 3.2 如果 X 的 uncle (叔叔) 是黑色,我们要分四种情况处理

      • 3.2.1 左左 (P 是 G 的左孩子,并且 X 是 P 的左孩子)
      • 3.2.2 左右 (P 是 G 的左孩子,并且 X 是 P 的右孩子)
      • 3.2.3 右右 (P 是 G 的右孩子,并且 X 是 P 的右孩子)
      • 3.2.4 右左 (P 是 G 的右孩子,并且 X 是 P 的左孩子)
      • 其实这种情况下处理就和我们的平衡二叉树一样了

    认识集合类

    集合表示一组对象,称为其元素。一些集合允许重复的元素,而另一些则不允许。一些集合是有序的,而其他则是无序的。

    集合类其实就是为了更好地组织、管理和操作我们的数据而存在的,包括列表、集合、队列、映射等数据结构。从这一块开始,我们会从源码角度给大家讲解(数据结构很重要!),不仅仅是教会大家如何去使用。

    集合类最顶层不是抽象类而是接口,因为接口代表的是某个功能,而抽象类是已经快要成形的类型,不同的集合类的底层实现是不相同的,同时一个集合类可能会同时具有两种及以上功能(既能做队列也能做列表),所以采用接口会更加合适,接口只需定义支持的功能即可。

    img

    数组与集合

    相同之处:

    1. 它们都是容器,都能够容纳一组元素。

    不同之处:

    1. 数组的大小是固定的,集合的大小是可变的。
    2. 数组可以存放基本数据类型,但集合只能存放对象。
    3. 数组存放的类型只能是一种,但集合可以有不同种类的元素。

    集合根接口Collection

    本接口中定义了全部的集合基本操作,我们可以在源码中看看。

    我们再来看看List和Set以及Queue接口。

    集合类的使用

    List列表

    首先介绍ArrayList,它的底层是用数组实现的,内部维护的是一个可改变大小的数组,也就是我们之前所说的线性表!跟我们之前自己写的ArrayList相比,它更加的规范,同时继承自List接口。

    先看看ArrayList的源码!

    基本操作

    List<String> list = new ArrayList<>();  //默认长度的列表List<String> listInit = new ArrayList<>(100);  //初始长度为100的列表
    

    向列表中添加元素:

    List<String> list = new ArrayList<>();list.add("lbwnb");list.add("yyds");list.contains("yyds"); //是否包含某个元素System.out.println(list);
    

    移除元素:

    public static void main(String[] args) {    List<String> list = new ArrayList<>();    list.add("lbwnb");    list.add("yyds");    list.remove(0);   //按下标移除元素    list.remove("yyds");    //移除指定元素    System.out.println(list);}
    

    也支持批量操作:

    public static void main(String[] args) {    ArrayList<String> list = new ArrayList<>();    list.addAll(new ArrayList<>());   //在尾部批量添加元素    list.removeAll(new ArrayList<>());   //批量移除元素(只有给定集合中存在的元素才会被移除)    list.retainAll(new ArrayList<>());   //只保留某些元素    System.out.println(list);}
    

    我们再来看LinkedList,其实本质就是一个链表!我们来看看源码。

    其实与我们之前编写的LinkedList不同之处在于,它内部使用的是一个双向链表:

    private static class Node<E> {    E item;    Node<E> next;    Node<E> prev;    Node(Node<E> prev, E element, Node<E> next) {        this.item = element;        this.next = next;        this.prev = prev;    }}
    

    当然,我们发现它还实现了Queue接口,所以LinkedList也能被当做一个队列或是栈来使用。

    public static void main(String[] args) {    LinkedList<String> list = new LinkedList<>();    list.offer("A");   //入队    System.out.println(list.poll());  //出队    list.push("A");    list.push("B");    //进栈    list.push("C");    System.out.println(list.pop());    System.out.println(list.pop());    //出栈    System.out.println(list.pop());}
    

    利用代码块来快速添加内容

    前面我们学习了匿名内部类,我们就可以利用代码块,来快速生成一个自带元素的List

    List<String> list = new LinkedList<String>(){{    //初始化时添加  this.add("A");  this.add("B");}};
    

    如果是需要快速生成一个只读的List,后面我们会讲解Arrays工具类。

    集合的排序

    List<Integer> list = new LinkedList<Integer>(){   //Java9才支持匿名内部类使用钻石运算符    {        this.add(10);        this.add(2);        this.add(5);        this.add(8);    }};list.sort((a, b) -> {    //排序已经由JDK实现,现在只需要填入自定义规则,完成Comparator接口实现  return a - b;    //返回值小于0,表示a应该在b前面,返回值大于0,表示b应该在a后面,等于0则不进行交换});System.out.println(list);
    

    迭代器

    集合的遍历

    所有的集合类,都支持foreach循环!

    public static void main(String[] args) {    List<Integer> list = new LinkedList<Integer>(){   //Java9才支持匿名内部类使用钻石运算符        {            this.add(10);            this.add(2);            this.add(5);            this.add(8);        }    };    for (Integer integer : list) {        System.out.println(integer);    }}
    

    当然,也可以使用JDK1.8新增的forEach方法,它接受一个Consumer接口实现:

    list.forEach(i -> {    System.out.println(i);});
    

    从JDK1.8开始,lambda表达式开始逐渐成为主流,我们需要去适应函数式编程的这种语法,包括批量替换,也是用到了函数式接口来完成的。

    list.replaceAll((i) -> {  if(i == 2) return 3;   //将所有的2替换为3  else return i;   //不是2就不变});System.out.println(list);
    

    Iterable和Iterator接口

    我们之前学习数据结构时,已经得知,不同的线性表实现,在获取元素时的效率也不同,因此我们需要一种更好地方式来统一不同数据结构的遍历。

    由于ArrayList对于随机访问的速度更快,而LinkedList对于顺序访问的速度更快,因此在上述的传统for循环遍历操作中,ArrayList的效率更胜一筹,因此我们要使得LinkedList遍历效率提升,就需要采用顺序访问的方式进行遍历,如果没有迭代器帮助我们统一标准,那么我们在应对多种集合类型的时候,就需要对应编写不同的遍历算法,很显然这样会降低我们的开发效率,而迭代器的出现就帮助我们解决了这个问题。

    我们先来看看迭代器里面方法:

    public interface Iterator<E> {  //...}
    

    每个集合类都有自己的迭代器,通过iterator()方法来获取:

    Iterator<Integer> iterator = list.iterator();   //生成一个新的迭代器while (iterator.hasNext()){    //判断是否还有下一个元素  Integer i = iterator.next();     //获取下一个元素(获取一个少一个)  System.out.println(i);}
    

    迭代器生成后,默认指向第一个元素,每次调用next()方法,都会将指针后移,当指针移动到最后一个元素之后,调用hasNext()将会返回false,迭代器是一次性的,用完即止,如果需要再次使用,需要调用iterator()方法。

    ListIterator<Integer> iterator = list.listIterator();   //List还有一个更好地迭代器实现ListIterator
    

    ListIterator是List中独有的迭代器,在原有迭代器基础上新增了一些额外的操作。


    Set集合

    我们之前已经看过Set接口的定义了,我们发现接口中定义的方法都是Collection中直接继承的,因此,Set支持的功能其实也就和Collection中定义的差不多,只不过使用方法上稍有不同。

    Set集合特点:

    • 不允许出现重复元素
    • 不支持随机访问(不允许通过下标访问)

    首先认识一下HashSet,它的底层就是采用哈希表实现的(我们在这里先不去探讨实现原理,因为底层实质上维护的是一个HashMap,我们学习了Map之后再来讨论)

    public static void main(String[] args) {    HashSet<Integer> set = new HashSet<>();    set.add(120);    //支持插入元素,但是不支持指定位置插入    set.add(13);    set.add(11);    for (Integer integer : set) {      System.out.println(integer);    }}
    

    运行上面代码发现,最后Set集合中存在的元素顺序,并不是我们的插入顺序,这是因为HashSet底层是采用哈希表来实现的,实际的存放顺序是由Hash算法决定的。

    那么我们希望数据按照我们插入的顺序进行保存该怎么办呢?我们可以使用LinkedHashSet:

    public static void main(String[] args) {    LinkedHashSet<Integer> set = new LinkedHashSet<>();  //会自动保存我们的插入顺序    set.add(120);    set.add(13);    set.add(11);    for (Integer integer : set) {        System.out.println(integer);    }}
    

    LinkedHashSet底层维护的不再是一个HashMap,而是LinkedHashMap,它能够在插入数据时利用链表自动维护顺序,因此这样就能够保证我们插入顺序和最后的迭代顺序一致了。

    还有一种Set叫做TreeSet,它会在元素插入时进行排序:

    public static void main(String[] args) {    TreeSet<Integer> set = new TreeSet<>();    set.add(1);    set.add(3);    set.add(2);    System.out.println(set);}
    

    可以看到最后得到的结果并不是我们插入顺序,而是按照数字的大小进行排列。当然,我们也可以自定义排序规则:

    public static void main(String[] args) {    TreeSet<Integer> set = new TreeSet<>((a, b) -> b - a);   //在创建对象时指定规则即可    set.add(1);    set.add(3);    set.add(2);    System.out.println(set);}
    

    现在的结果就是我们自定义的排序规则了。

    虽然Set集合只是粗略的进行了讲解,但是学习Map之后,我们还会回来看我们Set的底层实现,所以说最重要的还是Map。本节只需要记住Set的性质、使用即可。


    Map映射

    什么是映射

    我们在高中阶段其实已经学习过映射了,映射指两个元素的之间相互“对应”的关系,也就是说,我们的元素之间是两两对应的,是以键值对的形式存在。

    映射

    Map接口

    Map就是为了实现这种数据结构而存在的,我们通过保存键值对的形式来存储映射关系。

    我们先来看看Map接口中定义了哪些操作。

    HashMap和LinkedHashMap

    HashMap的实现过程,相比List,就非常地复杂了,它并不是简简单单的表结构,而是利用哈希表存放映射关系,我们来看看HashMap是如何实现的,首先回顾我们之前学习的哈希表,它长这样:

    img

    哈希表的本质其实就是一个用于存放后续节点的头结点的数组,数组里面的每一个元素都是一个头结点(也可以说就是一个链表),当要新插入一个数据时,会先计算该数据的哈希值,找到数组下标,然后创建一个新的节点,添加到对应的链表后面。

    而HashMap就是采用的这种方式,我们可以看到源码中同样定义了这样的一个结构:

    /** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */transient Node<K,V>[] table;
    

    这个表会在第一次使用时初始化,同时在必要时进行扩容,并且它的大小永远是2的倍数!

    /** * The default initial capacity - MUST be a power of two. */static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    

    我们可以看到默认的大小为2的4次方,每次都需要是2的倍数,也就是说,下一次增长之后,大小会变成2的5次方。

    我们现在需要思考一个问题,当我们表中的数据不断增加之后,链表会变得越来越长,这样会严重导致查询速度变慢,首先想到办法就是,我们可以对数组的长度进行扩容,来存放更多的链表,那么什么情况下会进行扩容呢?

    /** * The load factor for the hash table. * * @serial */final float loadFactor;
    

    我们还发现HashMap源码中有这样一个变量,也就是负载因子,那么它是干嘛的呢?

    负载因子其实就是用来衡量当前情况是否需要进行扩容的标准。我们可以看到默认的负载因子是0.75

    /** * The load factor used when none specified in constructor. */static final float DEFAULT_LOAD_FACTOR = 0.75f;
    

    那么负载因子是怎么控制扩容的呢?0.75的意思是,在插入新的结点后,如果当前数组的占用率达到75%则进行扩容。在扩容时,会将所有的数据,重新计算哈希值,得到一个新的下标,组成新的哈希表。

    但是这样依然有一个问题,链表过长的情况还是有可能发生,所以,为了从根源上解决这个问题,在JDK1.8时,引入了红黑树这个数据结构。

    当链表的长度达到8时,会自动将链表转换为红黑树,这样能使得原有的查询效率大幅度降低!当使用红黑树之后,我们就可以利用二分搜索的思想,快速地去寻找我们想要的结果,而不是像链表一样挨个去看。

    /** * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn * extends Node) so can be used as extension of either regular or * linked node. */static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    

    除了Node以外,HashMap还有TreeNode,很明显这就是为了实现红黑树而设计的内部类。不过我们发现,TreeNode并不是直接继承Node,而是使用了LinkedHashMap中的Entry实现,它保存了前后节点的顺序(也就是我们的插入顺序)。

    /** * HashMap.Node subclass for normal LinkedHashMap entries. */static class Entry<K,V> extends HashMap.Node<K,V> {    Entry<K,V> before, after;    Entry(int hash, K key, V value, Node<K,V> next) {        super(hash, key, value, next);    }}
    

    LinkedHashMap是直接继承自HashMap,具有HashMap的全部性质,同时得益于每一个节点都是一个双向链表,保存了插入顺序,这样我们在遍历LinkedHashMap时,顺序就同我们的插入顺序一致。当然,也可以使用访问顺序,也就是说对于刚访问过的元素,会被排到最后一位。

    public static void main(String[] args) {    LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, 0.75f, true);  //以访问顺序    map.put(1, "A");    map.put(2, "B");    map.put(3, "C");    map.get(2);    System.out.println(map);}
    

    观察结果,我们发现,刚访问的结果被排到了最后一位。

    TreeMap

    TreeMap其实就是自动维护顺序的一种Map,就和我们前面提到的TreeSet一样:

    /** * The comparator used to maintain order in this tree map, or * null if it uses the natural ordering of its keys. * * @serial */private final Comparator<? super K> comparator;private transient Entry<K,V> root;/*** Node in the Tree.  Doubles as a means to pass key-value pairs back to* user (see Map.Entry).*/static final class Entry<K,V> implements Map.Entry<K,V> {
    

    我们发现它的内部直接维护了一个红黑树,就像它的名字一样,就是一个Tree,因为它默认就是有序的,所以说直接采用红黑树会更好。我们在创建时,直接给予一个比较规则即可。

    Map的使用

    我们首先来看看Map的一些基本操作:

    public static void main(String[] args) {    Map<Integer, String> map = new HashMap<>();    map.put(1, "A");    map.put(2, "B");    map.put(3, "C");    System.out.println(map.get(1));    //获取Key为1的值    System.out.println(map.getOrDefault(0, "K"));  //不存在就返回K   	map.remove(1);   //移除这个Key的键值对}
    

    由于Map并未实现迭代器接口,因此不支持foreach,但是JDK1.8为我们提供了forEach方法使用:

    public static void main(String[] args) {    Map<Integer, String> map = new HashMap<>();    map.put(1, "A");    map.put(2, "B");    map.put(3, "C");    map.forEach((k, v) -> System.out.println(k+"->"+v));    	for (Map.Entry<Integer, String> entry : map.entrySet()) {   //也可以获取所有的Entry来foreach      int key = entry.getKey();      String value = entry.getValue();      System.out.println(key+" -> "+value);    }}
    

    我们也可以单独获取所有的值或者是键:

    public static void main(String[] args) {    Map<Integer, String> map = new HashMap<>();    map.put(1, "A");    map.put(2, "B");    map.put(3, "C");    System.out.println(map.keySet());   //直接获取所有的key    System.out.println(map.values());   //直接获取所有的值}
    

    再谈Set原理

    通过观察HashSet的源码发现,HashSet几乎都在操作内部维护的一个HashMap,也就是说,HashSet只是一个表壳,而内部维护的HashMap才是灵魂!

    // Dummy value to associate with an Object in the backing Mapprivate static final Object PRESENT = new Object();
    

    我们发现,在添加元素时,其实添加的是一个键为我们插入的元素,而值就是PRESENT常量:

    /** * Adds the specified element to this set if it is not already present. * More formally, adds the specified element <tt>e</tt> to this set if * this set contains no element <tt>e2</tt> such that * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>. * If this set already contains the element, the call leaves the set * unchanged and returns <tt>false</tt>. * * @param e element to be added to this set * @return <tt>true</tt> if this set did not already contain the specified * element */public boolean add(E e) {    return map.put(e, PRESENT)==null;}
    

    观察其他的方法,也几乎都是在用HashMap做事,所以说,HashSet利用了HashMap内部的数据结构,轻松地就实现了Set定义的全部功能!

    再来看TreeSet,实际上用的就是我们的TreeMap:

    /** * The backing map. */private transient NavigableMap<E,Object> m;
    

    同理,这里就不多做阐述了。

    JDK1.8新增方法使用

    最后,我们再来看看JDK1.8中集合类新增的一些操作(之前没有提及的)首先来看看compute方法:

    public static void main(String[] args) {    Map<Integer, String> map = new HashMap<>();    map.put(1, "A");    map.put(2, "B");    map.compute(1, (k, v) -> {   //compute会将指定Key的值进行重新计算,若Key不存在,v会返回null        return v+"M";     //这里返回原来的value+M    });  	map.computeIfPresent(1, (k, v) -> {   //当Key存在时存在则计算并赋予新的值      return v+"M";     //这里返回原来的value+M    });    System.out.println(map);}
    

    也可以使用computeIfAbsent,当不存在Key时,计算并将键值对放入Map

    public static void main(String[] args) {    Map<Integer, String> map = new HashMap<>();    map.put(1, "A");    map.put(2, "B");    map.computeIfAbsent(0, (k) -> {   //若不存在则计算并插入新的值        return "M";     //这里返回M    });    System.out.println(map);}
    

    merge方法用于处理数据:

    public static void main(String[] args) {    List<Student> students = Arrays.asList(            new Student("yoni", "English", 80),            new Student("yoni", "Chiness", 98),            new Student("yoni", "Math", 95),            new Student("taohai.wang", "English", 50),            new Student("taohai.wang", "Chiness", 72),            new Student("taohai.wang", "Math", 41),            new Student("Seely", "English", 88),            new Student("Seely", "Chiness", 89),            new Student("Seely", "Math", 92)    );    Map<String, Integer> scoreMap = new HashMap<>();    students.forEach(student -> scoreMap.merge(student.getName(), student.getScore(), Integer::sum));    scoreMap.forEach((k, v) -> System.out.println("key:" + k + "总分" + "value:" + v));}static class Student {    private final String name;    private final String type;    private final int score;    public Student(String name, String type, int score) {        this.name = name;        this.type = type;        this.score = score;    }    public String getName() {        return name;    }    public int getScore() {        return score;    }    public String getType() {        return type;    }}
    

    集合的嵌套

    既然集合类型中的元素类型是泛型,那么能否嵌套存储呢?

    public static void main(String[] args) {    Map<String, List<Integer>> map = new HashMap<>();   //每一个映射都是 字符串<->列表    map.put("卡布奇诺今犹在", new LinkedList<>());    map.put("不见当年倒茶人", new LinkedList<>());    System.out.println(map.keySet());    System.out.println(map.values());}
    

    通过Key获取到对应的值后,就是一个列表:

    map.get("卡布奇诺今犹在").add(10);System.out.println(map.get("卡布奇诺今犹在").get(0));
    

    让套娃继续下去:

    public static void main(String[] args) {    Map<Integer, Map<Integer, Map<Integer, String>>> map = new HashMap<>();}
    

    你也可以使用List来套娃别的:

    public static void main(String[] args) {    List<Map<String, Set<String>>> list = new LinkedList<>();}
    

    流Stream和Optional的使用

    Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

    img

    它看起来就像一个工厂的流水线一样!我们就可以把一个Stream当做流水线处理:

    public static void main(String[] args) {    List<String> list = new ArrayList<>();    list.add("A");    list.add("B");    list.add("C");    	//移除为B的元素  	Iterator<String> iterator = list.iterator();        while (iterator.hasNext()){            if(iterator.next().equals("B")) iterator.remove();        }    	//Stream操作    list = list     //链式调用            .stream()    //获取流            .filter(e -> !e.equals("B"))   //只允许所有不是B的元素通过流水线            .collect(Collectors.toList());   //将流水线中的元素重新收集起来,变回List    System.out.println(list);}
    

    可能从上述例子中还不能感受到流处理带来的便捷,我们通过下面这个例子来感受一下:

    public static void main(String[] args) {    List<Integer> list = new ArrayList<>();    list.add(1);    list.add(2);    list.add(3);  	list.add(3);    list = list            .stream()      			.distinct()   //去重(使用equals判断)            .sorted((a, b) -> b - a)    //进行倒序排列            .map(e -> e+1)    //每个元素都要执行+1操作            .limit(2)    //只放行前两个元素            .collect(Collectors.toList());    System.out.println(list);}
    

    当遇到大量的复杂操作时,我们就可以使用Stream来快速编写代码,这样不仅代码量大幅度减少,而且逻辑也更加清晰明了(如果你学习过SQL的话,你会发现它更像一个Sql语句)

    注意:不能认为每一步是直接依次执行的!

    List<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list.add(3);list = list        .stream()        .distinct()   //断点        .sorted((a, b) -> b - a)        .map(e -> {            System.out.println(">>> "+e);   //断点            return e+1;        })        .limit(2)   //断点        .collect(Collectors.toList());//实际上,stream会先记录每一步操作,而不是直接开始执行内容,当整个链式调用完成后,才会依次进行!
    

    接下来,我们用一堆随机数来进行更多流操作的演示:

    public static void main(String[] args) {    Random random = new Random();  //Random是一个随机数工具类    random            .ints(-100, 100)   //生成-100~100之间的,随机int型数字(本质上是一个IntStream)            .limit(10)   //只获取前10个数字(这是一个无限制的流,如果不加以限制,将会无限进行下去!)            .filter(i -> i < 0)   //只保留小于0的数字            .sorted()    //默认从小到大排序            .forEach(System.out::println);   //依次打印}
    

    我们可以生成一个统计实例来帮助我们快速进行统计:

    public static void main(String[] args) {    Random random = new Random();  //Random是一个随机数工具类    IntSummaryStatistics statistics = random            .ints(0, 100)            .limit(100)            .summaryStatistics();    //获取语法统计实例    System.out.println(statistics.getMax());  //快速获取最大值    System.out.println(statistics.getCount());  //获取数量    System.out.println(statistics.getAverage());   //获取平均值}
    

    普通的List只需要一个方法就可以直接转换到方便好用的IntStream了:

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.stream()
                .mapToInt(i -> i)    //将每一个元素映射为Integer类型(这里因为本来就是Integer)
                .summaryStatistics();
    }
    

    我们还可以通过flat来对整个流进行进一步细分:

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A,B");
        list.add("C,D");
        list.add("E,F");   //我们想让每一个元素通过,进行分割,变成独立的6个元素
        list = list
                .stream()    //生成流
                .flatMap(e -> Arrays.stream(e.split(",")))    //分割字符串并生成新的流
                .collect(Collectors.toList());   //汇成新的List
        System.out.println(list);   //得到结果
    }
    

    我们也可以只通过Stream来完成所有数字的和,使用reduce方法:

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        int sum = list
                .stream()
                .reduce((a, b) -> a + b)   //计算规则为:a是上一次计算的值,b是当前要计算的参数,这里是求和
                .get();    //我们发现得到的是一个Optional类实例,不是我们返回的类型,通过get方法返回得到的值
        System.out.println(sum);
    }
    

    通过上面的例子,我们发现,Stream不喜欢直接给我们返回一个结果,而是通过Optinal的方式,那么什么是Optional呢?

    Optional类是Java8为了解决null值判断问题,使用Optional类可以避免显式的null值判断(null的防御性检查),避免null导致的NPE(NullPointerException)。总而言之,就是对控制的一个判断,为了避免空指针异常。

    public static void main(String[] args) {
        String str = null;
        if(str != null){   //当str不为空时添加元素到List中
            list.add(str);
        }
    }
    

    有了Optional之后,我们就可以这样写:

    public static void main(String[] args) {
        String str = null;
        Optional<String> optional = Optional.ofNullable(str);   //转换为Optional
        optional.ifPresent(System.out::println);  //当存在时再执行方法
    }
    

    就类似于Kotlin中的:

    var str : String? = null
    str?.upperCase()
    

    我们可以选择直接get或是当值为null时,获取备选值:

    public static void main(String[] args) {
        String str = null;
        Optional optional = Optional.ofNullable(str);   //转换为Optional(可空)
        System.out.println(optional.orElse("lbwnb"));
     		// System.out.println(optional.get());   这样会直接报错
    }
    

    同样的,Optional也支持过滤操作和映射操作,不过是对于单对象而言:

    public static void main(String[] args) {
        String str = "A";
        Optional optional = Optional.ofNullable(str);   //转换为Optional(可空)
        System.out.println(optional.filter(s -> s.equals("B")).get());   //被过滤了,此时元素为null,获取时报错
    }
    
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        String str = "A";
        Optional optional = Optional.ofNullable(str);   //转换为Optional(可空)
        System.out.println(optional.map(s -> s + "A").get());   //在尾部追加一个A
    }
    

    其他操作自学了解。

    Arrays和Collections的使用

    Arrays是一个用于操作数组的工具类,它给我们提供了大量的工具方法:

    /**
     * This class contains various methods for manipulating arrays (such as
     * sorting and searching). This class also contains a static factory
     * that allows arrays to be viewed as lists. <- 注意,这句话很关键
     *
     * @author Josh Bloch
     * @author Neal Gafter
     * @author John Rose
     * @since  1.2
     */
    public class Arrays {
    

    由于操作数组并不像集合那样方便,因此JDK提供了Arrays类来增强对数组操作,比如:

    public static void main(String[] args) {
        int[] array = {1, 5, 2, 4, 7, 3, 6};
        Arrays.sort(array);   //直接进行排序(底层原理:进行判断,元素少使用插入排序,大量元素使用双轴快速/归并排序)
        System.out.println(array);  //由于int[]是一个对象类型,而数组默认是没有重写toString()方法,因此无法打印到想要的结果
        System.out.println(Arrays.toString(array));  //我们可以使用Arrays.toString()来像集合一样直接打印每一个元素出来
    }
    
    public static void main(String[] args) {
        int[] array = {1, 5, 2, 4, 7, 3, 6};
        Arrays.sort(array);
        System.out.println("排序后的结果:"+Arrays.toString(array));
        System.out.println("目标元素3位置为:"+Arrays.binarySearch(array, 3));  //二分搜素,必须是已经排序好的数组!
    }
    
    public static void main(String[] args) {
        int[] array = {1, 5, 2, 4, 7, 3, 6};
        Arrays
                .stream(array)    //将数组转换为流进行操作
                .sorted()
                .forEach(System.out::println);
    }
    
    public static void main(String[] args) {
        int[] array = {1, 5, 2, 4, 7, 3, 6};
        int[] array2 = Arrays.copyOf(array, array.length);  //复制一个一模一样的数组
        System.out.println(Arrays.toString(array2));
    
        System.out.println(Arrays.equals(array, array2));  //比较两个数组是否值相同
    
        Arrays.fill(array, 0);   //将数组的所有值全部填充为指定值
        System.out.println(Arrays.toString(array));
    
        Arrays.setAll(array2, i -> array2[i] + 2);  //依次计算每一个元素(注意i是下标位置)
        System.out.println(Arrays.toString(array2));   //这里计算让每个元素值+2
    }
    

    思考:当二维数组使用Arrays.equals()进行比较以及Arrays.toString()进行打印时,还会得到我们想要的结果吗?

    public static void main(String[] args) {
        Integer[][] array = {{1, 5}, {2, 4}, {7, 3}, {6}};
        Integer[][] array2 = {{1, 5}, {2, 4}, {7, 3}, {6}};
        System.out.println(Arrays.toString(array));    //这样还会得到我们想要的结果吗?
        System.out.println(Arrays.equals(array2, array));    //这样还会得到true吗?
    
        System.out.println(Arrays.deepToString(array));   //使用deepToString就能到打印多维数组
        System.out.println(Arrays.deepEquals(array2, array));   //使用deepEquals就能比较多维数组
    }
    

    那么,一开始提到的当做List进行操作呢?我们可以使用Arrays.asList()来将数组转换为一个 固定长度的List

    public static void main(String[] args) {
        Integer[] array = {1, 5, 2, 4, 7, 3, 6};
        List<Integer> list = Arrays.asList(array);   //不支持基本类型数组,必须是对象类型数组
        Arrays.asList("A", "B", "C");  //也可以逐个添加,因为是可变参数
    
        list.add(1);    //此List实现是长度固定的,是Arrays内部单独实现的一个类型,因此不支持添加操作
        list.remove(0);   //同理,也不支持移除
    
        list.set(0, 8);   //直接设置指定下标的值就可以
        list.sort(Comparator.reverseOrder());   //也可以执行排序操作
        System.out.println(list);   //也可以像List那样直接打印
    }
    

    文字游戏:allows arrays to be viewed as lists,实际上只是当做List使用,本质还是数组,因此数组的属性依然存在!因此如果要将数组快速转换为实际的List,可以像这样:

    public static void main(String[] args) {
        Integer[] array = {1, 5, 2, 4, 7, 3, 6};
        List<Integer> list = new ArrayList<>(Arrays.asList(array));
    }
    

    通过自行创建一个真正的ArrayList并在构造时将Arrays的List值传递。

    既然数组操作都这么方便了,集合操作能不能也安排点高级的玩法呢?那必须的,JDK为我们准备的Collocations类就是专用于集合的工具类:

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Collections.max(list);
        Collections.min(list);
    }
    

    当然,Collections提供的内容相比Arrays会更多,希望大家下去自行了解,这里就不多做介绍了。


    集合类编程实战

    反转链表

    1 <- 3 <- 5 <- 7 <- 9 转换为 1 <- 3 <- 5 <- 7 <- 9

    现在有一个单链表,尝试将其所有节点倒序排列

    public class Main {
        public static void main(String[] args) {
            Node head = new Node(1);
            head.next = new Node(3);
            head.next.next = new Node(5);
            head.next.next.next = new Node(7);
            head.next.next.next.next = new Node(9);
    
            head = reverse(head);
    
            while (head != null){
                System.out.println(head.value+" ");
                head = head.next;
            }
        }
    
        public static class Node {
            public int value;
            public Node next;
    
            public Node(int data) {
                this.value = data;
            }
        }
    
        public static Node reverse(Node head) {
            //在这里实现
        }
    }
    

    重建二叉树

    现在知道二叉树的前序: GDAFEMHZ,以及中序: ADEFGHMZ,请根据已知信息还原这颗二叉树。

    这里写图片描述

    实现计算器

    实现一个计算器,要求输入一个计算公式(含加减乘除运算符,没有负数但是有小数),得到结果,比如输入:1+4*3/1.321,得到结果为:2.2

    字符串匹配(KMP算法)

    现在给定一个主字符串和一个子字符串,请判断主字符串是否包含子字符串,例如主字符串:ABCABCDHI,子字符串:ABCD,因此主字符串包含此子字符串;主字符串:ABCABCUISA,子字符串:ABCD,则不包含。

    展开全文
  • 01数据库的实体关系

    2019-09-29 15:41:50
    如何分析实体集合之间的关系?  前提必须知道如何去分析实体?  解释 客观存在: 客观存在性是指某种事物具有不因外在因素或人的意愿而改变的性质  实体 和实体型 以及实体集的关系?  实体型用于区分不同...

    如何分析实体集合之间的关系

      前提必须知道如何去分析实体?

      解释 客观存在:
          客观存在性是指某种事物具有不因外在因素或人的意愿而改变的性质

      实体 和实体型 以及实体集的关系?

        实体型用于区分不同的实体,而实体集是一类实体的集合!

      需求分析中,分析实体,实体型。然后分析实体型之间的关系。

    1。实体:客观存在,可以相互区别的事物称为实体。(注意实体是客观上存在的). 2。实体型:用实体名及属性名集合来抽象和刻划的同类实体。比如,学生(学号,姓名) 3。实体集:性质相同的同类实体的集合,称为实体集。比如,(张三,李四) 4。联系:是指实体之间的相互关系。 5。E-R图:通常称为 实体-关系(联系)图,其实是不对的,严格的应该叫做实体型-关系(联系)图。因为E-R图讨论的实体不是具体的个体,而是同类实体的一个集合,即实体集,而实体型恰恰可以表达具有这类性质的集合。E-R图的作用是提供了表示实体型、属性和联系的方法。绘制E-R图的过程是在客观世界与抽象世界之间相互切换,并最终以抽象形式展现的结果。在E-R图中使用实体型来描述实体集(由客观世界实体抽象到抽象世界的实体型),考查客观中具体实体之间的关系并以联系来表示(由客观世界中实体之间的关系抽象到抽象世界的联系概念)。

         实体是单个个体  实体集合呢是同类实体的集合  

         实体型 是   同一类型的实体 不同的 实体信息 

      如: 商品 他是一个实体类型, 不同的实体 就是不同的实体  比如袜子衣服,这都是客观存在的事物 。区分统一实体类型的实体差异是 通过 相关的信息

        如 做月饼的模子 , 做出来的都是月饼 但是其中 成分含量是有区别的。

    如何通过E-R图分析实体型之间的关系?

      实体之间的关系通常图形描述 ?

          通常用矩形框代表实体,

            用椭圆形或圆角矩形表示实体的属性,  

          用菱形框表示实体的关系,

          并用直线把实体(或关系)与其属性连接起来。

     

    实体之间的联系有哪些?

      联系(Relationship): 数据对象彼此之间相互连接的方式称为联系,也称为关系。联系可分为以下 3 种类型: 
    (1) 一对一联系 (1 ∶ 1)  一对一的关系 一般不出现 因为数据库单表字段的关系就是一对一
      例如,一个部门有一个经理,而每个经理只在一个部门任职,则部门与经理的联系是一对一的。 

    一下关系是实体关系中经常出现的的实体关系
    (2) 一对多联系 (1 ∶ N)  主从关系
      例如,某校教师与课程之间存在一对多的联系“教”,即每位教师可以教多门课程,但是每门课程只能由一位教师来教【见图1】。  

       淘宝上的  登陆的用户 和创建的订单的关系   一个登陆的用户实体型可以创建多个订单实体型,但一个创建好的订单实体型 只属于一个用户实体型。  订单是一个创建的过程

      一对多 如何通过数据库表结构实现? d

         1.两个实体型对应两张表

        2.在多表的一方,添加一个外键 指向   主表的主见

      一对多 如何通过javabean对象来实现那?

        1.通过两个实体类建立两个javabean对象

        2.在主表中 添加一个集合 从表中类型的引用
    (3) 多对多联系 (M ∶ N) 
      例如,图1表示学生与课程间的联系(“ 学 ”)是多对多的,即一个学生可以学多门课程,而每门课程可以有多个学生来学。联系也可能有属性。例如,学生 “ 学 ” 某门课程所取得的成绩,既不是学生的属性也不是课程的属性。由于 “ 成绩 ” 既依赖于某名特定的学生又依赖于某门特定机的课程,所以它是学生与课程之间的联系 “ 学 ”的属性.

      淘宝 登陆的用户创建的订单 和 网站所拥有的商品是 一种  多对多的关系,  一个订单型中可以包含好多的商品型  ,一个商品信息 可以出现订单型中     

        比如我在淘宝买欧莱雅男士套装, 我登陆淘宝,我是一个用户。我可以创建多个订单来买欧莱雅男士套装  并付款,同一个商品信息可以出现在多个订单中,

      出现在多个订单的同一个商品怎么理解?

        统一中商品它有商品数量 。就像多个被克隆的人。

      如何分析 学生和选课之间的多对多的关系

          1. 学生必须是多个,课程必须是多个

          2.每一个学生可以选择多个课程 单个学生对课程来说是 主从的关系 一对多

          3.每个课程可以选择多个学生  单个课程 对学生来说也是主从的关系 一对多

       如何分析  订单和商品的多对多的关系?

          1.订单可以有多个。商品种类有多个。

          2.一个订单可以包含多个商品种类  

          3.同一个商品种类 可以出现在多个订单中   

      多对多 如何通过数据库表结构实现? 将多对多的关系 变成两个一对多的关系

         1.两个实体型对应两张表  加一个关系表

        2.关系表中 存放两个实体型的主见

      多对多 如何通过javabean对象来实现那?

        1.通过两个实体类建立两个javabean对象

        2.两个javabean中都添加对方引用的集合

     

                              

     

     

    ER 图 讨论的是  实体集合之间的关系

          但是这个关系那是有每一个实体之间个关系体现的!

     

    转载于:https://www.cnblogs.com/nextgg/p/7746432.html

    展开全文
  • (3)实体集:具有相同类型及相同性质实体集合。实体集可以相交(例如销售部门员工既属于销售员工实体集也属于员工实体集)。 2)属性 (1)属性:实体集中每个实体都具有的特征描述。 (2)属...
  • E-R实体关系介绍

    千次阅读 2018-11-06 17:28:39
    E-R图也称实体-联系图(Entity Relationship Diagram),提供了表示实体类型、属性和联系的方法,用来描述现实世界的概念模型。 它是描述现实世界关系概念模型的有效方法。是表示概念关系模型的一种方式。用“矩形框...
  • java集合总结

    2020-12-20 22:43:15
    集合类存放于Java.util包中,主要有3种:set(集)、list(列表包含Queue)和map(映射)。 Collection:Collection是集合List、Set、Queue的基本的接口。 Iterator:迭代器,可以通过迭代器遍历集合中的数据 Map:是...
  • JAVA 集合

    2021-08-27 22:25:05
    集合类存放于 Java.util 包中,主要有 3 种:set(集)、list(列表包含 Queue)和 map(映射)。 Collection:Collection 是集合 List、Set、Queue 的最基本的接口。 Iterator:迭代器,可以通过迭代器遍历集合中的...
  • 拓扑的定义和性质

    2021-10-07 18:08:40
    拓扑是研究几何图形或空间在连续改变形状后还能保持不变的一些性质的一个学科。它只考虑物体间的位置关系而不考虑它们的形状和大小。 拓扑英文名是Topology,直译是地志学,最早指研究地形、地貌相类似的有关学科。...
  • Java集合

    2021-07-29 10:46:57
    前言 集合框架是基础知识中的核心部分,并且应用十分广泛。若要理解、掌握,还得从集合的原理和特性等入手。下面是我整理和自己理解的内容。...集合大体分为单列集合和双列集合。 1.单列集合 ...
  • mongodb 映射实体 MongoDB简介 当今的NoSQL格局包括许多非常有能力的竞争者,它们以多种不同方式解决大数据问题。 这些竞争者之一就是功能强大的MongoDB 。 MongoDB是一种面向文档的无模式存储解决方案,使用JSON...
  • Java基础 集合

    2021-08-08 23:01:32
    Java基础 集合 集合类存放于Java.util包中,主要有3种:Set(集),list(列表,包含Queue),map(映射,(k,v)) 文章目录Java基础 集合一、接口继承关系和实现1.List2.Set2.1 HashSet(Hash表):2.2 TreeSet(二叉树)2.3...
  • 实体型:具有相同属性的实体,具有由相同的特征和性质,用实体名及其属性名集合来抽象和刻画同类实体,称为实体型; 实体集:同型实体集合称为实体集; 属性:通常指表的一列; 联系:实体不会是单独存在的,...
  • Java进阶——集合

    2021-10-17 11:05:05
    Java进阶——集合框架 1、集合框架简介 集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下。 Java集合框架(Java Collections Framework,JCF)是为表示和操作集合而规定的一种统一的标准的...
  • 文章目录一,实体集1,实体集表示为表2,有复合属性的实体集3,有多值属性的实体集二,联系集1,联系集的定义与性质2,联系集的度三,映射基数四,键、码五,约束1,基数约束2,参与约束3,关系约束的另一种表示法六...
  • JAVA集合知识点整理

    2020-03-26 22:08:49
    集合类存放于 Java.util 包中, 主要有 3 种: set(集)、 list(列表包含 Queue) map(映射) 1. Collection: Collection 是集合 List、 Set、 Queue 的最基本的接口。 2. Iterator:迭代器,可以...
  • 康托尔集合论-罗素悖论-公理化集合论-不完全性定理 1.第二次数学危机的解决---集合论成了全部数学的基础。 (第二次数学危机详细见参考中三次数学危机.) 19世纪,柯西详细而有系统地发展了极限理论。柯西认为把无穷小...
  • 3. JAVA 集合 3.1.**接口继承关系和实现** 集合类存放于 Java.util 包中,主要有 3 种:set(集)、list(列表包含 Queue)和 map(映射)。 \1. Collection:Collection 是集合 List、Set、Queue 的最基本的接口。 ...
  • 【数据库】实体之间的联系

    千次阅读 2020-05-09 18:21:01
    **实体型:**具有相同属性的实体具有相同的特征和性质,用实体名及其属性名集合来抽象和刻画同类实体实体集:同型实体集合。 二、实体之间的联系: 一般地,把参与联系的实体型的数目称之为联系的度。 一个实体...
  • java 集合

    2021-02-24 09:07:09
    0java 集合 Collection是单列集合的根接口,主要用于存储一系列符合某种规则的元素,它有两个重要的子接口List和Set。 List接口的特点是元素有序可重复 Set接口的特点是元素无序,不可重复 ArrayList和LinkedList...
  • JAVA集合

    2021-01-11 14:33:43
    文章目录接口继承关系和实现ListArrayList(数组)Vector(数组实现,线程同步)LinkedList(链表)SetHashSet(Hash表)TreeSet(二叉树)...集合类存放在Java.util包中,主要有3种:set(集),list(列表包含Queue)
  • E-R图也称实体-联系图(Entity Relationship Diagram),提供了表示实体类型、属性和联系的方法,用来描述现实世界的概念模型。1、表示方法E-R是描述现实世界概念结构模型的有效方法。是表示概念模型的一种方式。(1)...
  • 波与实体运动的区别波动三要素 波动三要素 由上面几个自然现象可以归纳出波的运动(以下简称波动)有三个基本要素组成,分别是波源、介质和波。以水波和声波形式的波动为例,将触水的石块和汽车喇叭归为波源,以水面...
  • 数据库复习(4) 实体关系模型

    千次阅读 2020-07-14 16:30:39
    实体集是具有共同性质同类实体集合 实体集的属性类型划分 简单的和复合的(conposite,如名字等等) 单值的/多值的(如电话号码等) 直接的/派生的(可以由其他属性计算得来,如生日等) 关系集(待补充) E-...
  • 2020最新Java集合面试题

    千次阅读 2020-05-12 18:39:50
    另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆 栈、队列和双向队列使用 13、什么Set集合 Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素...
  • 实体集:一类实体集合。10 个人放在一起就是 “人” 的实体集。 在同一个实体集里面的实体都有着相同的属性, 每一个实体集中的实体都有一个 key 来唯一标识(这是为了以后设计出来数据库之后能够唯一搜索到这个...
  • ER图(实体关系图)怎么画?

    万次阅读 多人点赞 2020-11-27 19:56:32
    E-R图又称实体关系图,是一种提供了实体,属性和联系的方法,用来描述现实世界的概念模型。通俗点讲就是,当我们理解了实际问题的需求之后,需要用一种方法来表示这种需求,概念模型就是用来描述这种需求。 比如...
  • 浅谈嵌套命名实体识别(Nested NER)

    千次阅读 2020-04-06 13:11:58
    ©PaperWeekly 原创 ·作者|张成蹊单位|北京大学硕士生研究方向|自然语言处理序命名实体识别(Named Entity Recognition, 下称 NER)任务,主要目...
  • 在 Java2中,有一套设计优良的接口和类组成了Java集合框架Collection,使程序员操作成批的数据或对象元素极为方便。这些接口和类有很多对抽象数据类型操作的API,而这是我们常用的且在数据结构中熟知的。例如Map,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,700
精华内容 5,480
关键字:

性质相同的同类实体的集合