栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java知识点

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java知识点

Java知识点 1.OOP介绍 (1)OOP是什么:

面向对象程序设计(Object Oriented Programming),简称OOP,也就是我们常说的面向对象编程。面向对象编程是一种计算机编程架构,它的一条基本原则是程序由单个能够起到子程序作用的单元或对象组合而成。

(2)面向对象与面向过程的区别:

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

例如五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。

面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

可以说面向过程就是自顶向下的编程,而面向对象就是高度实物抽象化。

(3)OOP三大特性: a.封装:

也称为信息隐藏,就是将一个类的使用和实现分开,只保留部分接口和方法与外部联系,或者说只公开了一些供开发人员使用的方法。于是开发人员只需要关注这个类如何使用,而不用去关心其具体的实现过程。

b.继承:

就是子类自动继承其父级类中的属性和方法,并可以添加新的属性和方法或者对部分属性和方法进行重写。继承增加了代码的可重用性。

c.多态:

子类继承了来自父级类中的属性和方法,并对其中部分方法进行重写。于是多个子类中虽然都具有同一个方法,但是这些子类实例化的对象调用这些相同的方法后却可以获得完全不同的结果,这种技术就是多态性。多态性增强了软件的灵活性。

(4)使用OOP用什么好处: a.易维护

采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。

b.质量高

在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量。

c.效率高

在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。

d.易扩展

由于继承、封装、多态的特性,使得系统更灵活、更容易扩展,而且成本较低。

2.继承和接口实现的区别 (1)抽象层次不同。

抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

(2)跨域不同。

抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。抽象类是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可,但是接口不同。实现它的子类可以不存在任何关系和共同之处。例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞这个接口,具备飞的行为,这里我们总不能将鸟、飞机共用一个父类吧!所以说抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在”is-a” 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已,相当于是”like-a”的关系。

(3)设计层次不同。

对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。比如我们有两个动物类,猫、狗在这里,我们在抽象他们的共同点形成动物抽象类。但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。

3.形参传基本类型和引用类型的区别 (1)形参传基本类型

当形式参数为基本类型时,传递过来的是一个值。

方法在调用后,会在栈空间开辟一个空间,创建一个局部变量,然后将接受到的值复制到形式参数的变量上,然后对其进行操作。在方法结束时,创建的局部变量也会消失。同时其原始数据并没有收到影响。

(2)形参传引用类型

当形式参数为引用类型时,传递过来的值是一个堆内存的地址。

调用方法后,系统会在栈空间开辟一个空间,创建一个对象,当接收到地址值后,会将刚创建的对象指向地址,然后方法对引用类型的操作实际上操作的是在堆空间存放的原始数据。当方法结束后,方法中创建的对象排队释放,而原始的引用类型依然指向堆空间,所以原始数据发生了变化。

4.JAVA异常类 (1)什么是异常

异常就是在运行时产生的问题。通常用Exception描述。

在java中,把异常封装成了一个类,当出现问题时,就会创建异常类对象并抛出异常相关的信息(如详细信息,名称以及异常所处的位置)。

(2)异常的继承关系
Throwable: 它是所有错误与异常的超类
        |- Error 错误
        |- Exception 异常
(3)异常与错误的区别

异常是指程序在编译或者运行时出现的某种异常问题,我们可以对异常进行某种处理,如果不处理异常的话,程序将会停止运行。

错误是指程序在运行时出现的严重问题,无法处理,程序将会停止运行。Error通常都是系统级别的问题,都是jvm所在系统发生的,只能通过修改源代码解决问题。

(4)异常产生的过程 a.运行或编译时产生异常 b.创建异常类的对象 c.将异常类对象传给调用者(main()方法)处理 d.调用者无法处理,再将异常类对象传给jvm虚拟机 e.jvm将异常类的信息(名称、详细信息、异常所处的位置)打印在屏幕上,并且停止程序的运行 (5)对异常的处理 a.throws:抛出异常
修饰符 返回值类型 方法名称(参数)throws 异常1名称,异常2名称{
 }
b.try…catch…finally:捕获异常
try {
      //需要被检测的语句。
}
catch(异常类 变量) { 
      //异常的处理语句。
}
finally {
      //一定会被执行的语句。
}

注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

(6)异常在方法重写中的细节 a.子类覆盖父类方法时,如果父类的方法声明异常,子类只能声明父类异常或者该异常的子类,或者不声明。 b.当被覆盖的方法没有异常声明时,子类覆盖时无法声明异常。 5.Java出异常怎么保证资源关闭 (1)try-catch-finally
try {
    获取资源
} catch (Exception e) {
    捕获异常
} finally {
    释放资源
}
(2)try-with-resources
try(获取资源){
   执行逻辑
}
6.Java对象创建流程 (1)Java普通对象的创建(不包括数组和Class对象) a.new指令

虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那么须先执行相应的类加载过程。

b.分配内存

接下来虚拟机将为新生代对象分配内存。对象所需的内存的大小在类加载完成后便可完全确定。

c.初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

d.对象的初始设置

接下来虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码等信息。这些信息存放在对象的对象头之中。

e.init方法

在上面的工作都完成了之后,从虚拟机的角度看,一个新的对象已经产生了,但是从Java程序的角度看,对象创建才刚刚开始:init方法还没有执行,所有的字段都还为零。所以,一般来说,执行new指令后会接着执行init方法,把对象按照程序员的意愿进行初始化(应该是将构造函数中的参数赋值给对象的字段),这样一个真正可用的对象才算完全产生出来。

7.Java内存区域描述

Java内存区域通常包括这几个部分:程序计数器、栈、方法区、堆。

(1)程序计数器

是用来指示执行哪条指令的。

由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。

(2)栈

栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。

(3)堆

Java中的堆是用来存储对象本身以及数组(当然,数组引用是存放在Java栈中的)。另外,堆是被所有线程共享的,在JVM中只有一个堆。

(4)方法区

方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中。

8.Java怎么实现并发控制

并行程序开发不可避免地要涉及多线程、多任务的协作和数据共享等问题。在JDK中,提供了多种途径实现多线程间的并发控制。比如常用的:可重入锁、读写锁和信号量。

(1)重入锁 a.可重入锁和不可重入锁 I.可重入锁

当线程在访问A方法的时候,获取A方法的锁,然后访问B方法获取B方法的锁,并计数加1,以此类推可以访问完了以后依次解锁。

可重入锁模型:{{{{}}}} 每次都可访问另一个方法,且加锁计数器加1,完全释放锁为计数器等于0

II.不可重入锁

当线程在访问A方法的时候,获取的A方法的锁,在A方法锁释放之前不能够访问其他方法(如方法B)的锁。

不可重入锁模型:{}{}{}{}{}都是独立的访问每一个方法,加锁 - 释放;加锁 - 释放…

注意:不可重入锁容易发生阻塞

b.可重入锁的分类 I.reentrantLock(显式的可重入锁)

与synchronized的区别是需要手动释放锁

II.synchronized(隐式的可重入锁) i.修饰一个代码块
synchronized(this)
{
    //code...
}

一个线程访问一个对象中的synchronized修饰的代码块时,其他试图访问该对象的线程将被阻塞。

注意:1.只有是同一个对象才会被锁定
     2.非synchronized代码块不受影响
ii.修饰一个方法
public synchronized void method(){}

类似于修饰一个代码块,只是作用范围变成整个方法。
注意:synchronized关键字不能被继承

iii.修饰一个静态的方法

静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。

iv.修饰一个类

效果和修饰静态方法类似。即:chronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。

(2)读写锁(也成为共享锁和排他锁) 特点: a.在同一时刻可以允许多个读线程访问,在写线程访问的时候其他的读线程和写线程都会被阻塞。

读写锁维护一对锁(读锁和写锁),通过锁的分离,使得并发性提高。

b.支持重入:

读线程获取读锁之后能够再次获取读锁,写线程获取写锁之后能再次获取写锁,也可以获取读锁。对应的获取次数保存在本地线程中,由线程自身维护该值。

源码:

ReadWriteLock接口中有两个方法,分别是readLock和writeLock

(3)信号量

Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

9.一个变量怎么保证线程安全性 (1)几种变量的线程安全情况 a.静态变量:线程非安全

原因:被static修饰的成员变量独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。

b.实例变量:单例时线程非安全,非单例时线程安全

原因:实例变量是实例对象私有的,系统只存在一个实例对象,则在多线程环境下,如果值改变后,则其它对象均可见,故线程非安全;如果每个线程都在不同的实例对象中执行,则对象与对象间的修改互不影响,故线程安全。

补充:单例模式和多例模式(设计模式中的知识点)
所谓单例就是所有的请求都用一个对象来处理,比如我们常用的service和dao层的对象通常都是单例的,而多例则指每个请求用一个新的对象来处理,比如action

c.局部变量:线程安全

原因:每个线程执行时都会把局部变量放在各自的帧栈的内存空间中,线程间不共享,故不存在线程安全问题。

(2)使用上题中的重入锁、读写锁和信号量等 10.介绍一下线程池

在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。创建并销毁线程的过程势必会消耗内存,而内存资源是及其宝贵的,所以,我们就提出了线程池的概念。

线程池通常会维护一些线程(数量为corePoolSize),这些线程被重复使用来执行不同的任务,任务完成后不会销毁。在待处理任务量很大的时候,通过对线程资源的复用,避免了线程的频繁创建与销毁,从而降低了系统资源消耗。

12.怎么去设计一个线程池
新任务提交->
     当前线程数是否小于核心线程数 ?
      创建核心线程,执行任务 :
      任务队列是否可以添加任务 ?(任务队列有上限)
        任务进入队列,等待空闲线程,执行任务 :
        当前线程数是否小于最大线程数 ?
          创建普通线程,执行任务 :
          拒绝任务
13.什么是线程安全问题?怎么解决? (1) 什么是线程安全问题

当多个线程同时共享同一个变量,做写的操作时,可能会发生数据冲突问题,这就是线程安全问题。

(2)怎么解决

见第8题和第9题

14.乐观锁悲观锁介绍一下 (1)作用

无论是乐观锁还是悲观锁,实际上都是为了实现并发控制,如在 DBMS 中确保多个事务同时增删改查同一数据时,不破坏事务的隔离性、一致性和数据库的统一性。

(2)区别

乐观锁比较适用于读多写少的情况(多读场景),悲观锁比较适用于写多读少的情况(多写场景)。

(3)悲观锁 a.介绍

在修改数据之前先锁定,再修改。之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式,总是假设最坏的情况。

  • 优点:为数据处理的安全提供了保证。
  • 缺点:在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
b.实现(以mysql为例)

要使用悲观锁,必须关闭 MySQL 数据库的自动提交属性set autocommit=0。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。

以电商下单扣减库存的过程说明一下悲观锁的使用:

在对 id = 1 的记录修改前,先通过 for update 的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。

(4)乐观锁 a.介绍

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。

  • 优点:不会产生死锁
b.实现

主要就是两个步骤:冲突检测和数据更新。

乐观锁可以使用CAS(Compare And Swap)方式实现

CAS 即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值(V)与预期原值(A)相匹配,那么处理器会自动将该位置值更新为新值(B)。否则,处理器不做任何操作。当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

比如前面的扣减库存问题,通过乐观锁可以实现如下:

在更新之前,先查询一下库存表中当前库存数(quantity),然后在做 update 的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

  • 但是,以上更新语句存在一个比较严重的问题,即ABA问题:

    比如说线程一从数据库中取出库存数 3,这时候线程二也从数据库中取出库存数 3,并且线程二进行了一些操作变成了 2。然后线程二又将库存数变成 3,这时候线程一进行 CAS 操作发现数据库中仍然是 3,然后线程一操作成功。

尽管线程一的 CAS 操作成功,但是不代表这个过程就是没有问题的。

一个比较好的解决办法,就是通过一个单独的可以顺序递增的 version 字段。优化如下:

乐观锁每次在执行数据修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题。除了 version 以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

以上 SQL 其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。为了最大程度地提升吞吐率,提高并发能力!可以修改如下:

c.典型实现

Java 并发包下面提供了一系列的 Atomic 原子类,比如说 AtomicInteger:

//import java.util.concurrent.atomic.AtomicInteger;
public static void main(String[] args) {
    public static AtomicInteger count = new AtomicInteger(0);
    public static void increase() {
        count.incrementAndGet();
    }
}

多个线程可以并发的执行 AtomicInteger 的 incrementAndGet(),意思就是把 count 的值累加 1,接着返回累加后最新的值。

(5)二者的选择 a.相应效率

如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。但更新失败的概率就会比较高,容易发生业务失败。

b.冲突频率

如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。

c.重试代价

如果重试代价大,建议采用悲观锁。悲观锁更新失败的概率比较低。

15.hash冲突怎么处理?除了拉链法你还知道哪些? (1)拉链法 (2)开放地址法

当发生地址冲突的时候,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。

有几种常用的探查序列的方法:

  • 线性探查:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
  • 二次探查:冲突发生时,在表的左右进行跳跃式探测。di = 12, -12, 22, -22 … k2, -k^2 (k <= m/2)
  • 伪随机探测:冲突发生时,按照伪随机数查找空单元
(3)再hash法

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

(4)建立公共溢出区

另设一个溢出表,一旦发生hash冲突,都填入此表。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/276862.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号