Java总结

Java的四个特性(抽象、封装、继承、多态),对多态的理解(多态的实现方式)

  • 抽象:抽象是将一类对象的共同特征总结出来构造类的过程。包括数据抽象和行为抽象两个方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

  • 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类),继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。

  • 封装:通常认为封装是将数据与操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法是对实现细节的一种封装;我们编写的一个类就是对数据和数据操作的封装。封装就是隐藏一切可隐藏的东西,只向外提供简单的编程接口。

  • 多态

    • 方法重载(overload)实现的是编译时的多态性(也成为前向绑定)
    • 方法重写(overwrite)实现的是运行时的多态性(也成为后向绑定)
    • 多态的实现方式:方法重写,子类继承父类并重写父类中已有的或抽象的方法;对象构造,用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为
    • 举例:有两种客户:订购客户和卖方客户,两个客户都可以登录系统,他们有相同的方法login,但登录之后他们会进入不同的页面,也就是在登录后有不同的操作行为。两种客户都继承父类的Login方法,但对于不同的对象,拥有不同的操作。

面向对象和面向过程的区别?

  • 面向过程就像是一个细心的管家,事无具细的都要考虑到。而面向对象就像是一个家用电器,你只需要知道他的功能,不需要知道他的工作原理

  • 面向过程是一种以时间为中心的编程思想,就是分析出解决问题所需的步骤,然后用函数把这些步骤实现,并按顺序调用。面向对象是以“对象”为中心的编程思想

  • 举例:汽车发送、汽车到站

    • 对于面向过程来说,这是两个事件,面向过程编程我们关心的是事件,而不是汽车本身。针对上述过程,形成两个函数,依次调用
    • 对于面向对象来说,我们关心的是汽车这类对象,两个事件只是这类对象所具有的行为。而且对于这两个行为没有顺序要求

重载与重写

  • 重载:重载发生在同一个类中,同名的方法如果有不同的参数列表(参数类型、参数个数或者两者都不同)则视为重载

  • 重写:重写发生在子类与父类之间,重写要求子类重写方法与父类被重写方法具有相同的返回参数,比父类方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则),根据不同的子类对象确定调用哪个对象

面向对象开发的六个基本准则(单一职责、开放封闭、里氏替换、依赖倒置、合成聚合复用、接口隔离),迪米特法则

  • 单一职责:一个类只能它该做的事情(高内聚),在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就是单一职责

  • 开放封闭:软件实体应当对扩展开放,对修改关闭。要做到开闭有两点:抽象是关键,一个系统如果没有抽象类或者接口系统就没有扩展点;封装可变性,将系统中可变因素封装在一个继承结构中,如果多个可变因素混杂在一起,系统将复杂而混乱

  • 里氏替换:任何时候都可以用子类替换父类,子类一定是增加了父类的能力而不是减少,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用没有问题

  • 依赖倒置:面向接口编程(声明方法的参数类型、方法返回类型,变量的引用类型要尽量使用抽象类型而不是具体类型,因为抽象类型可以被其子类型所替代)

  • 合成聚合复用:优先使用聚合或合成关系复用代码

  • 接口隔离:接口不要大而全,要小而专,一个接口只应该描述一种能力,接口也应该是高内聚的

迪米特法则:又称为最少知识原则,一个对象应当对其他对象有尽可能少的了解

static与final的区别

static:

  • 修饰变量:静态变量随着类加载时完成初始化,内存中只有一个,且JVM只会为它分配一次内存,所有类共享静态变量
  • 修饰方法:在类加载的时候就存在,不依赖任何实例;static方法必须实现,不能用abstract修饰
  • 修饰代码块:在类加载完成后就会执行代码块中的内容
  • 父类静态代码块->子类静态代码块->父类非静态代码块->父类构造方法->子类非静态代码块->子类构造方法

final:

  • 修饰变量
    • 编译期常量:类加载的过程完成初始化,编译后带入到任何计算式中,只能是基本类型
    • 运行时常量:基本数据类型或引用数据类型,引用不可变,但引用的对象内容可变
  • 修饰方法:不能被继承,不能被子类修改
  • 修饰类:不能被继承
  • 修饰新参:final形参不可变

HashMap和Hashtable的区别,HashMap中的key可以是任何对象或数据类型吗?

区别:

  • Hashtable的方法是同步的,HashMap未经同步,所以在多线程环境下要手动同步HashMap,这个区别就相当于Vector和ArrayList
  • Hashtable不允许使用null(key和value都不可以),HashMap允许null值(key只能有一个null值,value没有限制)
  • 两者的遍历方式大同小异,Hashtable仅仅比HashMap多一个elements方法
    Hashtable和HashMap都能通过values()方法返回一个Collection,然后遍历
    两者也都可以通过entrySet()返回一个Set,然后遍历
  • Hashtable使用Enumeration,HashMap使用Iterator
  • 哈希值计算方法不同,hashtable直接使用对象的hashCode,hashMap重新计算hash值,而且用于代替求模
  • Hashtable的hash数组大小是11,增加方式是old*2+1,hashMap的默认大小是16,而且一定是2的指数
  • hashtable是基于Dictionary,hashMap是基于AbstractMap类

HashMap的key可以为null,但不可以为可变对象。如果是可变对象时,对象的属性改变,则对象的hashCode也进行了相应的改变,导致下次无法查找到已存在Map的数据。

如果可变对象被用作HashMap的键时,在改变对象的状态时,不要再改变其哈希值,我们只需要保证成员变量的改变能保证该对象的哈希值不变即可。

hashtable是线程安全的,其实是在对应的方法添加了synchronized关键字进行修饰,由于在执行此方法的时候需要获得对象锁,则执行起来比较慢。所以可以使用concurrentHashMap

HashMap与concurrentHashMap

  1. 区别
    HashMap与concurrentHashMap的区别

  2. concurrentHashMap线程安全吗,如何保证线程安全?

    • hashtable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问hashtable的线程都必须竞争同一把锁,当容器中有多把锁时,每一把锁用于锁住容器中的一部分数据,那么当多线程访问容器内不同数据段的数据时,线程间不会存在锁竞争,从而提高了并发访问的效率,这也是concurrentHashMap采用的锁分段技术,当一个线程访问其中一个段的数据时,其他段的数据也能被其他线程所访问

    • get的高效之处:get过程不需要加锁,除非读到的值是空的才需要加锁重读
      concurrentHashMap的get方法高效之处

    • put方法首先定位到segment,然后在segment里进行插入操作
      concurrentHashMap的put操作步骤

String、StringBuffer、StringBuilder

String、StringBuffer、StringBuilder

String的不变性

String重写了Object的hashCode和toString,重写equals()不重写hashCode()有什么问题?

  • 当equals方法被重写,通常需要重写hashCode方法,以保证两个相等的对象必须有相同的hashCode

    • object1.equals(object2)时为true,则object1.hashCode()==object2.hashCode()也为true
    • object1.hashCode()==object2.hashCode()为false,则 object1.equals(object2)一定为false
    • object1.hashCode()==object2.hashCode()为true,则object1.equals(object2)状态不确定
  • 在存储散列集合(如Set)时,如果原对象.equals(新对象),但如果没有对hashCode重写,即两个对象拥有不同的hashCode,则在集合中会存储两个值相同的对象,从而导致混淆。因此,重写equals方法时,必须重写hashCode方法

Java序列化

  1. 定义:将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复到原来的对象,序列化可以弥补不同操作系统之间的差异

  2. 作用

    • Java远程方法调用
    • 对JavaBeans进行序列化
  3. 序列化与反序列化
    序列化与反序列化

注意事项:

  • 被static修饰的属性不会被序列化
  • 对象的类名、属性都会被序列化,但是方法不会被序列化
  • 要保证对象所在的类的属性可以被序列化
  • 当通过网络、文件进行序列化时,必须按照写入的顺序读取对象
  • 反序列化时必须有序列化对象时的class文件
  • 最好显示的声明serializableID,因为在不同的JVM时,默认生成的serializableID不同,可能导致反序列化失败

多线程

多线程

线程安全

  • 定义:某个类的行为与其规范一致;不管多个线程是怎样的执行顺序和优先级,或是wait、sleep、join等,如果一个类在多线程访问下都正常,并且不需要额外的同步处理和协调,那么这就是线程安全。
  • 如何保证线程安全:对变量使用volitate;对程序段进行加锁(synchronized、lock)
  • 注意:非线程安全的集合可以在多线程中使用,但是不能用作多个线程共享的属性,只能作为某个线程独享的属性

多线程共用一个变量

线程池

定义:实现先创建若干个可执行的线程放入一个容器(池)中,当需要的时候,不用自行创建只需要从容器中获取,使用完毕后不需要销毁而是放入到容器中,从而减少创建和销毁线程对象的开销。

设计线程池
创建线程池

volatile关键字与synchronized的区别

区别

sleep()与wait()

  • sleep是Thread类的方法
  • wait是Object类的方法
  • 区别:
    • sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)
    • wait()是object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态

synchronized与lock

synchronized与lock的区别

synchronized与static synchronized的区别:

  • synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized锁,同一个类的不同实例之间没有这种约束
  • static synchronized是控制类的所有实例的访问,限制线程同时访问JVM中该类的所有实例同时访问对应的代码块

异常

  • Throwable是Java语言中所有错误和异常的超类,它有两个子类:ErrorException
  • 异常种类
    • Error:错误,是程序无法处理的,如OutOfMemeryError、ThreadDeath等,这种情况下交由JVM处理,一般会选择终止线程
    • Exception:程序可以处理的异常,它又分为CheckedException(受检异常)和UnCheckedException(不受检异常)
      • CheckedException:发生在编译阶段,必须使用try...catch...或者throws,否则编译不通过。
      • UnCheckedException:发生在运行期,一般由程序的逻辑问题引起。

Java中的NIO、BIO、AIO分别是什么?

  1. BIO

    • 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销,可以通过线程池机制来改善
    • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中
  2. NIO

    • 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时,才启动一个线程进行处理
    • NIO方式适用于连接数多且连接比较短的架构,比如聊天服务器,并发局限于应用中
  3. AIO

    • 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理
    • AIO方式适用于连接数目多且连接比较长的架构,比如相册服务器,充分调用OS参与并发操作

Java内存模型(Java Memory model,JMM)

对于并发编程,有两个关键问题:线程之间的通信和同步

线程之间的通信

线程的通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种共享内存和消息传递。
典型的共享内存通信方式就是通过共享对象来进行通信。典型的消息传递方式就是wait()和notify()。

线程之间的同步

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
在共享内存并发模型里,同步是显式进行的。程序必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java的并发采用的是共享内存模型

JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来讲,JMM定义了线程和主内存之间的抽象关系:**线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。**本地内存是JMM的一个抽象概念,并不真实存在。

线程之间的通信方式:首先,线程A把本地内存A中更新过的共享变量刷新到主内存中。然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证

JVM对Java内存模型的实现

在JVM内部,Java内存模型把内存分成了两个部分:线程栈区和堆区。下图展示了Java内存模型在JVM中的逻辑视图:

JVM中运行的每个线程都拥有自己的线程栈,线程栈包括了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的执行,调用栈会不断的变化。

线程栈还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,**线程中的本地变量对其他线程是不可见的。**即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈创建本地变量,因此,每个线程中的本地变量都有自己的版本。
所有原始类型(boolean、byte、short、int、long、char、float、double)的本地变量都直接保存在线程栈中,对于它们的值各个线程都相互独立。对于原始类型的本地变量,一个线程可以传递一个副本给另一个线程,当它们之间是无法共享的。

堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。

  • 一个本地变量如果是原始类型,那么它会被完全存储到栈区。
  • 一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
  • 对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。
  • 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。
  • Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。

堆中的对象可以被多线程共享。如果一个线程获得一个对象的应用,它便可访问这个对象的成员变量。如果两个线程同时调用了同一个对象的同一个方法,那么这两个线程便可同时访问这个对象的成员变量,但是对于本地变量,每个线程都会拷贝一份到自己的线程栈中。

下图是上面的全部过程:

共享对象的可见性与竞争现象

volatile关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU内存屏障指令实现的。
synchronized代码块可以保证同一个时刻只能有一个线程进入代码竞争区,synchronized代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会flush到主存,不管这些变量是不是volatile类型的。

volatile和synchronized的区别

首先理解线程安全的两个方面:执行控制和内存可见
执行控制的目的是控制代码执行顺序及是否可以并发执行。
内存可见指的是线程执行结果在内存中对其他线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

synchronized关键字解决的是执行控制的问题,它会阻止其他线程获得当前对象的监控锁,这样使得当前被synchronized修饰的代码块无法被其他线程访问,也就无法并发执行。更为重要的是,synchronized还会创建内存屏障,内存屏障指令保证了所有CPU操作结果都会刷新到主存中,从而保证了内存可见性,同时也使得先获得这个锁的线程都happens-before与随后获得这个锁的线程。

volatile关键字解决的是内存可见的问题,该关键字会使得被修饰的变量读写都会直接刷新到主存中,这样就保证了内存可见。该种方式可以保证内存可见性但是对读取没有要求的需求中。

使用volatile仅能实现对原始变量(如short、boolean、int等)操作的原子性,但是不能保证复合操作的原子性。例如,对于i++,实际上是由多个原子操作组成,如果使用多个线程去操作i++,则只能保证他们所操作的变量i在同一块内存区域,但是存在写入脏数据的情况。

使用volatile关键字要满足以下条件:

  • 对变量的写入操作不依赖与变量的当前值,或者可以确保只有单个线程在操作变量
  • 该变量没有包含在具有其他变量的不变式中

总结如下:
volatile与synchronized的区别

Java内存模型的基础原理

  • 指令重排序:
    为了提高程序的执行性能,编译器和处理器都会对指令进行重排序,但是JMM确保在不同的编译器和处理器平台下,通过插入指定类型的内存屏障来禁止编译器重排序和处理器重排序,为上层提供一致的内存可见性保证。

指令序列的重排序

  • 数据依赖性:
    如果两个操作在访问同一个变量,且这两个操作有一个是写操作,此时两个操作之间存在数据依赖性。

编译器重排序和处理器重排序不会改变数据依赖性关系的两个操作之间的执行顺序,即不会重排序。

注意:
这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器和线程间之间的数据依赖性不被编译器和处理器所考虑。

  • as-if-serial*
    不过怎么重排序,单线程下的执行结果不能改变,编译器、runtime(运行时)和处理器都必须遵守as-if-serial语义

内存屏障(Memory Barrier)

内存屏障

volatile内存语义的实现:
volatile内存语义的实现

对于volatile关键字,JMM采用保守策略

happens-before策略

happens-before规则

注意:
两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

解决hash冲突的几种方法

开放地址法

  1. 线性探测法:ThreadLocalMap
    插入元素时,如果发生冲突,算法会简单的从该槽位置向后循环遍历hash表,直到找到表中的下一个空槽,并将该元素放入该槽中(会导致相同hash值的元素挨在一起和其他hash值对应的槽被占用)。 查找元素时,首先散列值所指向的槽,如果没有找到匹配,则继续从该槽遍历hash表,直到:(1)找到相应的元素;(2)找到一个空槽,指示查找的元素不存在,(所以不能随便删除元素);(3)整个hash表遍历完毕(指示该元素不存在并且hash表是满的)
    用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:
    ① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简单的结构是顺序表,查找方法可用顺序查找。
    ② 按上述算法建立起来的哈希表,删除工作非常困难。如果将此元素删除,查找的时会发现空槽,则会认为要找的元素不存在。只能标上已被删除的标记,否则,将会影响以后的查找。
    ③ 线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。

  2. 线程补偿探测法
    线性补偿探测法的基本思想是:将线性探测的步长从 1 改为 Q ,即将上述算法中的
    hash = (hash + 1) % m 改为:hash = (hash + Q) % m = hash % m + Q % m,**而且要求 Q 与 m 是互质的,**以便能探测到哈希表中的所有单元。

  3. 伪随机探测
    随机探测的基本思想是:将线性探测的步长从常数改为随机数,即令: hash = (hash + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。

拉链法

HashMap
拉链法的优点
与开放定址法相比,拉链法有如下几个优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

拉链法的缺点
 拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

再散列(双重散列、多重散列)

当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。

建立一个公共溢出区

假设哈希函数的值域为[0,m-1],则设向量HashTable[0…m-1]为基本表,另外设立存储空间向量OverTable[0…v]用以存储发生冲突的记录。

Author: Toyan
Link: https://toyan.top/java-summary/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
支付宝打赏
微信打赏