2018年Java面试汇总(还有最近一些面试问题未总结,下次更新)

再次更新,面试中主要是问基础和项目。想要找到工作,首先基础要扎实,然后有一个拿得出手的项目(拿不出也无所谓咯)。对于Java面试的这些常见问题一定要弄懂。
另外还有数据结构、算法部分,则需要另外去复习,这里只有Java方面。

Java基础

1. Java四大特性

封装,继承,多态和抽象

封装(模块化)

封装给对象提供了隐藏内部特性和行为的能力。对象提供一些能被其他对象访问的方法来改
变它内部的数据。在 Java 当中,有 3 种修饰符: public, private 和 protected。每一种修饰符
给其他的位于同一个包或者不同包下面对象赋予了不同的访问权限。
下面列出了使用封装的一些好处:

通过隐藏对象的属性来保护对象内部的状态。
提高了代码的可用性和可维护性,因为对象的行为可以被单独的改变或者是扩展。
禁止对象之间的不良交互提高模块化

继承(可复用性)

继承给对象提供了从基类获取字段和方法的能力。继承提供了代码的重用,也可以在不修改类的情况下给现存的类添加新特性。

父类的静态方法能否被子类重写
不能,重写只适用于实例方法,不能用于静态方法。

父类子类的初始化顺序(笔试常考)

  1. 父类中静态成员变量和静态代码块
  2. 子类中静态成员变量和静态代码块
  3. 父类中普通成员变量和代码块,父类的构造函数
  4. 子类中普通成员变量和代码块,子类的构造函数

总结:

  1. 在类加载的时候执行父类的static代码块,并且只执行一次
  2. 执行子类的static代码块,并且只执行一次
  3. 执行父类的类成员初始化,并且是从上往下按出现顺序执行
  4. 执行父类的构造函数
  5. 执行子类的类成员初始化,并且从上往下按出现顺序执行
  6. 执行子类的构造函数
多态(可扩展性)(问的无敌多)

多态是编程语言给不同的底层数据类型做相同的接口展示的一种能力。一个多态类型上的操作可以应用到其他类型的值上面。

多态是指一个引用类型在不同情况下的多种形态,同一个行为有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作。
也可以理解为,多态是指父类引用变量通过指向不同的子类对象来调用不同子类实现的方法。

多态的好处

  1. 可替换性
  2. 可扩充性
  3. 接口性
  4. 灵活性
  5. 简化性

多态存在的三个必要条件:

  • 继承
  • 重写
  • 父类引用指向子类对象

实现多态的方式主要有以下三种方式

  1. 接口实现
  2. 继承父类重写方法
  3. 同一类中进行方法重载

JVM如何实现多态的
多态允许具体访问时实现方法的动态绑定。Java对于动态绑定的实现主要依赖于方法表,通过继承和接口的多态实现有所不同。

继承:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法。

接口:Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表。

具体实现见JVM部分
Java技术–多态的实现原理

举例回答:什么是多态?
多态,顾名思义就是多种形态,指一个行为在不同情况下具有不同的形态,比如说一个接口通过使用不同的实例来执行不同的操作(一个父类引用变量可以通过指向不同的子类对象来调用不同的方法)。这是通过多个子类继承父类,并分别重写父类的方法实现的。

抽象

抽象是把想法从具体的实例中分离出来的步骤,因此,要根据他们的功能而不是实现细节来创建类。 Java 支持创建只暴漏接口而不包含方法实现的抽象的类。这种抽象技术的主要目的是把类的行为和实现细节分离开。


Java中的8种基本数据类型,以及各占的字节:(被问过几次)
  • 数值型
    • 整数类型:byte(1)、short(2)、int(4)、long(8)
    • 浮点类型:float(4)、double(8)
  • 字符型:char(2)
  • 布尔型:boolean(1B)
String能被继承吗?

不能,因为String类有final修饰符,所以不能继承。

String、Stringbuffer、stringbuilder的区别 --TODO
  • String 字符串常量(值不可变,每次对String的操作都会生成新的String对象)
  • StringBuilder 字符串变量(Java5、非线程安全,效率高,速度快)
  • StringBuffer 字符串变量(单线程、线程安全、效率低、速度慢)

多数情况下使用StringBuilder,但在应用程序要求线程安全时,必须使用StringBUffer


2. 抽象类和接口有什么区别
比较抽象类接口
默认方法可以有默认的方法实现不存在方法的实现
实现方式extendsimplements
构造器可以不可以
和正常类的区别抽象类不能被实例化接口则是完全不同的类型
访问修饰符public/protected/default默认public不能用其他
多继承一个子类只能存在一个父类一个子类可以存在多个接口
添加新方法可以不修改子类子类必须重新实现该方法
接口的意义

规范、扩展、回调

抽象的意义
  1. 为其它子类提供一个公共的类型
  2. 封装子类中重复定义的内容
  3. 定义抽象方法,子类虽然有不同的实现,但是定义时一样

接口和类的区别:

  • 接口中定义的属性必须是public abstract的,而接口中的方法则必须也是public abstract。
  • 接口可以被多个类实现,而且必须实现接口中的所有抽象方法,否则只能声明为抽象类。
  • 接口可以多重实现,即一个接口可以同时实现多个接口。
  • 接口也可以多重继承,实现多态机制。

abstract-class的使用场景:
一句话,在既需要统一的接口,又需要实例变量或缺省的方法的情况下,就可以使用它。最常见的有:

  • A.定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。可以用abstract-class定义一组方法体,甚至可以是空方法体,然后由子类选择自己所感兴趣的方法来覆盖。
  • B.某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还必需类中表示状态的变量来区别不同的关系。abstract的中介作用可以很好地满足这一点.
  • C.规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能
异常

Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

Exception(异常):是程序本身可以处理的异常。
包括RuntimeException运行异常,描述编程错误。

免检异常和必检异常
在教材上,免检异常指得是RuntimeException、Error和他们的子类。

java反射
1. 理解Class类

对象照镜子后可以得到的信息:某个类的数据成员名、方法和构造器、某个类到底实现了哪些接口。

Class是什么?

Class是一个类。
对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个类的有关信息。
  –Class 对象只能由系统建立对象
  –一个类在 JVM 中只会有一个Class实例
  –每个类的实例都会记得自己是由哪个 Class 实例所生成

Class这个类封装了什么信息?

1. Class是一个类,封装了当前对象所对应的类的信息
  2. 一个类中有属性,方法,构造器等,比如说有一个Person类,一个Order类,一个Book类,这些都是不同的类,现在需要一个类,用来描述类,这就是Class,它应该有类名,属性,方法,构造器等。Class是用来描述类的类
  3. Class类是一个对象照镜子的结果,对象可以看到自己有哪些属性,方法,构造器,实现了哪些接口等等
  4. 对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个类的有关信息。
5. Class 对象只能由系统建立对象,一个类(而不是一个对象)在 JVM 中只会有一个Class实例

对象为什么需要照镜子(反射)呢?

1. 有可能这个对象是别人传过来的
  2. 有可能没有对象,只有一个全类名 ,通过反射,可以得到这个类里面的信息

获取Class对象的三种方式

1.通过类名获取 类名.class
  2.通过对象获取 对象名.getClass() 这种方式是用在传进来一个对象,却不知道对象类型的时候使用
  3.通过全类名获取 Class.forName(全类名) 一般框架开发中这种用的比较多

Class clazz = null;
// 1. 通过类名
clazz = Person.class;
// 2. 通过对象名
clazz = person.getClass();
// 3. 通过全类名
clazz = Class.forName("com.test.Person");
Class类常用的方法

forName() 返回指定类名name的Class对象
newInstance() 调用缺省构造函数,返回该Class对象的一个实例
newInstance(Object[] args)
getName()
//getSuperClass() 返回父类CLass //没有这个类,搞错了吧
getSuperclass() 返回超类Class
getInterfaces() 返回接口
getClassLoader() 返回类加载器

2. 理解ClassLoader

类装载器是用来把类(class)装载进 JVM 的。JVM 规范定义了两种类型的类装载器:启动类装载器(bootstrap)和用户自定义装载器(user-defined class loader)。 JVM在运行时会产生3个类加载器组成的初始化加载器层次结构

类加载器的原理
类加载器是一个用来加载类文件的类。java源代码通过javac编译器编译类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。有三种默认使用的类加载器:
Bootstrap类加载器
负责加载rt.jar中的JDK类文件,它是所有类加载器的父加载器。

Extension类加载器
Extension将加载类的请求先委托给它的父加载器(Bootstrap),

System类加载器

类加载器的作用

Java类加载器的作用就是在运行时加载类。Java类加载器基于三个机制:委托、可见性和单一性。

委托机制是指将加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它。

可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。

单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。正确理解类加载器能够帮你解决NoClassDefFoundError和java.lang.ClassNotFoundException,因为它们和类的加载相关。

3.反射
反射概述

Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的內部信息,并能直接操作任意对象的内部属性及方法。

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

Java反射机制主要提供了以下功能:
在运行时构造任意一个类的对象
在运行时获取任意一个类所具有的成员变量和方法
在运行时调用任意一个对象的方法(属性)
**生成动态代理**(实现AOP)
Class 是一个类; 一个描述类的类.封装了
描述方法的 Method,
描述字段的 Filed,
描述构造器的 Constructor 等属性.
Method
获取
getMethods()
getDeclareMethods()
getDeclareMethod()

使用
method.invoke(obj,args)

Field
获取
getDeclareFields()
getDeclareField("name")

使用
field.get()
field.set(obj,"name")
field.setAccessible(true) // 设置私有属性可访问
Constructor
获取
getConstructors()
getConstructor(String.class, int.class) // 根据参数列表获取指定构造器
调用构造器的newInstance()方法创建对象
Object obj = constructor.newInstance(args[])
Annotation

如何描述注解

Java代理模式

代理的基本构成: 抽象角色、代理角色、真实角色

分为静态代理和动态代理

静态代理:
interface Subject {

}

JDK动态代理:

1. JDK动态代理步骤
  1. 创建一个实现InvocationHandler接口的类,并必须实现invoke方法
  2. 创建被代理的类及接口
  3. 调用Proxy的静态方法,创建一个代理类
  4. 通过代理调用方法
2. 实现InvocationHandler接口
    public class DynaProxyHello implements InvocationHandler{
        
        private Object target;//目标对象
        /**
         * 通过反射来实例化目标对象
         * @param object
         * @return
         */
        public Object bind(Object object){
            this.target = object;
            return Proxy.newProxyInstance(this.target.getClass().getClassLoader(), this.target.getClass().getInterfaces(), this);
        }
        
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            Object result = null;
            Logger.start();//添加额外的方法
            //通过反射机制来运行目标对象的方法
            result = method.invoke(this.target, args);
            Logger.end();
            return result;
        }
        
    }
JVM体系结构

开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。

JVM处在核心位置,是程序与底层操作系统和硬件无关的关键。

JVM工作原理和流程
Java工作流程

JVM工作流程

JVM在整个jdk中处于最底层,负责于操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机. 操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.

1.创建JVM装载环境和配置
2.装载JVM.dll
3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
4.调用JNIEnv实例装载并处理class类。

Java代码编译和执行的整个过程包含了以下三个重要的机制:

  • Java源码编译机制
  • 类加载机制
  • 类执行机制

类加载机制:
类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

JVM的类加载是通过ClassLoader及其子类来完成的

JVM内存配置参数

整个堆的大小 = 年轻代大小 + 年老代大小

-Xms JVM启动申请的初始Heap值
(默认空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,所以Server端JVM最好将-Xms和-Xmx设为相同值,避免每次垃圾回收完成后JVM重新分配内存)

-Xmx JVM可申请的最大Heap值

-Xmn Java Heap Young区大小 (官方推荐为整个堆得3/8)

-Xss Java每个线程的Stack大小

JVM内存管理 --内存的分配和回收
3.1 内存分配

JVM内存:

  • 线程共享
    • 方法区 —保存方法代码(编译后的java代码)和符号表。存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息
    • 堆/Java堆 —存储对象实例和数组值(通过new创建的对象的内存都在此分配)
  • 非线程共享
    • 本地方法区 – 用于支持native方法的执行,存储了每个native方法调用的状态。
    • 程序计数器(PC寄存器)
    • JVM方法栈/Java虚拟机栈 – 局部变量

Java堆:
Java堆可以分为新生代、老年代
新生代又可以分为Eden空间、From Survivor空间、To Survivor空间

内存分配:
Java对象所占用的内存主要是从堆中进行分配。堆是所有线程共享的。因此在堆上分配内存时需要加锁,这导致了创建对象的开销比较大。当堆空间不足时,会触发GC。如果GC后空间仍然不足,则会抛出OutOfMemory错误信息。

3.2 内存回收 --垃圾回收机制(GC)

Java 提供了一种垃圾回收机制,在后台创建一个守护进程。该进程会在内存紧张的时候自动跳出来,把堆空间的垃圾全部进行回收,从而保证程序的正常运行。

  • 垃圾:不再存活的对象
    • 垃圾的判断方法:
      • 引用计数法 —引用数为0的
      • 可达性分析 —不可达的
  • 回收
    • 标记-清理
    • 标记-整理
    • 复制
    • 分代垃圾回收机制

可达性分析:
对象之间的引用可以抽象成树形结构,通过树根(GC Roots)作为起点,从这些树根往下起点,从这些树根往下搜索,搜索走过的链称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明这个对象是不可用的,该对象会被判定可回收的对象。

可作为GC Roots的对象有

  1. 虚拟机栈()中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(Native方法)引用的对象

三种算法的优劣

  1. 标记-清理: 实现简单,与保守式的GC(对象不能被移动)兼容,但容易造成磁盘碎片化,分配速度比较慢,与写时复制不兼容,并且标记,清理效率都不高。

  2. 标记-整理: 避免了内存碎片。

  3. 复制: 效率快,但空间代价过高

现在收集器都采用的是分代收集算法:
年轻代通常使用时间占优的GC,因为年轻代的GC非常频繁 。(复制)
年老代通常使用善于处理大空间的GC,因为年老代的空间大,GC频率低 。(标记整理)

新生代采用复制:
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个,这两个区分别叫做From survivor和To survivor),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。

一般情况下,From中的对象(算法会考虑经过GC幸存的次数)到一定次数(阈值(如果说每次GC之后这个对象依旧在Survive中存在,GC一次他的Age就会加1,默认15就会放到OldGeneration。

如果没到次数From中的对象会被复制到To中,复制完成后To中保存的是有效的对象,Eden和From中剩下的都是无效的对象,这个时候就把Eden和From中所有的对象清空。在复制的时候Eden中的对象进入To中,To可能已经满了,这个时候Eden中的对象就会被直接复制到Old Generation中,From中的对象也会直接进入Old Generation中。

年老代:

  1. 被复制到老年代的对象大于老年代剩余空间,就会触发full gc
  2. 如果有永久代,在不足够分配时,也会触发Full GC;
  3. 调用System.gc(),提醒JVM FullGC,但不可控

永久代:
Permanent Generation(永久代)可以理解成方法区,(它属于方法区)也有可能发生GC,例如类的实例对象全部被GC了,同时它的类加载器也被GC掉了,这个时候就会触发永久代中对象的GC。

minor GC:
一般情况下,当新对象生成,并且在Eden申请空间失败时,minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC:
标记-整理
因为要对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

JVM垃圾回收期串行、并行、并发垃圾回收机制

3.3 内存泄漏

内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。
因为GC只清理未被引用的对象,如果一个对象已经被使用完了,但仍然被引用着,GC无法回收该对象,就是内存泄漏。

注意和内存溢出的区别:内存溢出,要求分配的内存超出了系统能给你的。比如说数组越界。

3.4 数组在JVM中的存储

已知栈用来保存局部变量,局部变量包括基本类型和引用类型。对于基本类型来说,
栈保存的是它的值,而对于引用类型来说,存放的是它的地址,当一个局部变量使用完之后立马就会被释放,
但是堆区不会立即被释放。

而堆,存放的是动态产生的数据,比如new出来的对象,每个对象都有自己独立的
堆块,其中保存着实例变量。因为同一个类的方法都是共享的,方法只有在

equals()和hashCode()方法
官方文档定义

hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。

hashCode 的常规协定是:
1. 在 Java应用程序执行期间,在同一对象上多次调用hashCode方法时,必须一致地返回相同的整数,前提是对象上equals比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
2. 如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用hashCode方法都必须生成相同的整数结果。

以下情况不是必需的:如果根据equals(java.lang.Object)方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。 实际上,由Object类定义的hashCode方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)

3. 当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

为什么当equals方法被重写时,通常有必要重写 hashCode 方法?
如果不重写的话,就会违反第二条。假设两个对象,重写了其equals方法,其相等条件是属性相等,就返回true。如果不重写hashcode方法,其返回的依然是两个对象的内存地址值,必然不相等。这就出现了equals方法相等,但是hashcode不相等的情况。

重写equals()方法就必须重写hashCode()方法主要是针对HashSet和Map集合类型。集合框架只能存入对象(对象的引用(基本类型数据:自动装箱))。
在向HashSet集合中存入一个元素时,HashSet会调用该对象(存入对象)的hashCode()方法来得到该对象的hashCode()值,然后根据该hashCode值决定该对象在HashSet中存储的位置。

在Map集合中,例如其子类Hashtable,HashMap,存储的数据是<key,value>对,key,value都是对象,被封装在Map.Entry,即:每个集合元素都是Map.Entry对象。在Map集合中,判断key相等标准也是:两个key通过equals()方法比较返回true,两个key的hashCode的值也必须相等。判断valude是否相等equal()相等即可。

重写hashCode()的原则
(1)同一个对象多次调用hashCode()方法应该返回相同的值;
(2)当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()应该返回相等的(int)值;
(3)对象中用作equals()方法比较标准的Filed(成员变量(类属性)),都应该用来计算hashCode值。

对于第一个原则,可以想得到,如果重写的hashCode()是根据对象的字段来获取的,那么当对象字段被改变后,hashCode也将随之变化。可能造成HashMap中的值无法被取出。

equals()和==的区别

基本数据类型使用==来比较值
对于复合数据类型,使用==,比较的是内存中的存放地址。
而equals在Object中的定义是比较对象的内存地址,但在一些类库中被覆盖了,比较的不再是堆内存中的存放地址。比如说,String,Integer,Date等。

String s1 = "a";
String s2 = "a";
String s3 = new String("a");

s1 == s2 为真
s1 == s3 为假
s1.equals(s3) 为真

程序在运行的时候会创建一个字符串缓冲池当使用s2=“a”,这样的表达是创建字符串的时候,程序首先会在这个String缓冲池中寻找相同值的对象,在第一个程序中,s1先被放到了池中,所以在s2被创建的时候,程序找到了具有相同值的 s1将s2引用s1所引用的对象"a"

如果使用new操作符,则是明确重新创建一个对象。

编码
  1. Ascii/GBK/Unicode/Utf-8
编码说明
ASCII1个字节,127
GB23132个字节,GB2312 是对 ASCII 的中文扩展。
线程
1. 创建线程的方式 --三种
  • 继承thread类创建线程类
  • 通过Runnable接口创建线程类
  • 通过Callable和future创建线程
1.1 继承thread类创建线程类

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。

package com.thread;
public class FirstThreadTest extends Thread{
    int i = 0;
    //重写run方法,run方法的方法体就是现场执行体
    public void run()
    {
        for(;i<100;i++){
          System.out.println(getName()+"  "+i);
        }
    }
    public static void main(String[] args)
    {
        for(int i = 0;i< 100;i++)
        {
            System.out.println(Thread.currentThread().getName()+"  : "+i);
            if(i==20)
            {
                new FirstThreadTest().start();
                new FirstThreadTest().start();
            }
        }
    }
}

上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。getName()方法返回调用该方法的线程的名字。

1.2 通过Runnable接口创建线程类

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。

package com.thread;
public class RunnableThreadTest implements Runnable
{
    private int i;
    public void run()
    {
        for(i = 0;i <100;i++)
        {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
    public static void main(String[] args)
    {
        for(int i = 0;i < 100;i++)
        {
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==20)
            {
                RunnableThreadTest rtt = new RunnableThreadTest();
                new Thread(rtt,"新线程1").start();
                new Thread(rtt,"新线程2").start();
            }
        }
    }
}
1.3 通过Callable和future创建线程

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

package com.thread;
 
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class CallableThreadTest implements Callable<Integer>
{
    public static void main(String[] args)
    {
        CallableThreadTest ctt = new CallableThreadTest();
        FutureTask<Integer> ft = new FutureTask<>(ctt);
        for(int i = 0;i < 100;i++)
        {
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
            if(i==20)
            {
                new Thread(ft,"有返回值的线程").start();
            }
        }
        try
        {
            System.out.println("子线程的返回值:"+ft.get());
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        } catch (ExecutionException e)
        {
            e.printStackTrace();
        }
    }
    @Override   // 表明这是重写父类的方法,编译器就会检查重写是否符合规范,如果不加该注解,函数名写错了,编译器也不会有任何提醒。
    public Integer call() throws Exception
    {
        int i = 0;
        for(;i<100;i++)
        {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
}
创建线程的三种方式的对比

采用实现Runnable、Callable接口的方式创见多线程时,优势是:

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

使用继承Thread类的方式创建多线程时优势是:

编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:

线程类已经继承了Thread类,所以不能再继承其他父类。

线程通信
线程池

所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务。线程池的关键在于它为我们管理了多个线程,我们不需要关心如何创建线程,我们只需要关系我们的核心业务,然后需要线程来执行任务的时候从线程池中获取线程。任务执行完之后线程不会被销毁,而是会被重新放到池子里面,等待机会去执行任务。

常见线程池
sleep() 、join()、yield()有什么区别

1、sleep()方法

在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 让其他线程有机会继续执行,但它并不释放对象锁。也就是如果有Synchronized同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常

比如有两个线程同时执行(没有Synchronized),一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完成后,低优先级的线程才能执行;但当高优先级的线程sleep(5000)后,低优先级就有机会执行了。
总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。

2、yield()方法

yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。

3、join()方法

Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。

Thread t = new MyThread(); t.start(); t.join();

保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。

Java并发编程:线程池的使用

线程池的几种方式
newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程

newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制

newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行

newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

举个栗子

private static final Executor exec=Executors.newFixedThreadPool(50);

Runnable runnable=new Runnable(){
public void run(){

}
}
exec.execute(runnable);

Callable callable=new Callable() {
public Object call() throws Exception {
return null;
}
};

Future future=executorService.submit(callable);
future.get(); // 等待计算完成后,获取结果
future.isDone(); // 如果任务已完成,则返回 true
future.isCancelled(); // 如果在任务正常完成前将其取消,则返回 true
future.cancel(true); // 试图取消对此任务的执行,true中断运行的任务,false允许正在运行的任务运行完成
参考:

创建线程池的几种方式

线程的生命周期

新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态

(1)生命周期的五种状态

新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
例如:Thread t1=new Thread();

就绪(runnable)
线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();

运行(running)
线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。

死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

自然终止:正常运行run()方法后终止

异常终止:调用stop()方法让一个线程终止运行

堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。

正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。

正在等待:调用wait()方法。(调用motify()方法回到就绪状态)

被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)

参考:

线程的生命周期

锁机制
说说线程安全问题
线程安全是指要控制多个线程对某个资源的有序访问或修改,而在这些线程之间没有产生冲突。
在Java里,线程安全一般体现在两个方面:
1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable(后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。
2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。

线程并发、线程同步、线程安全、线程通信

多线程编程中的三个核心概念:

  1. 原子性:一个操作(可能包含多个子操作),要么全部执行,要么全部都不执行
  2. 可见性:多个线程访问共享变量时,一个线程对共享变量的修改,其他线程能够立即看到
  3. 顺序性:

Java如何解决多线程并发问题:
如何保证原子性:
锁和同步
Lock
Synchronized
CAS AtomicInteger 原子操作类

Java如何保证可见性
volatile关键字保证可见性

Java如何保证顺序性

如何保证线程安全
  1. 多实例、多副本 THreadLocal
  2. 锁机制Synchronize、lock方式
  3. 使用java.util.concurrent下面的类库
1. ThreadLocal:

为每一个线程维护一个副本变量,从而让线程拥有私有的资源,就不再需要去竞争进程中的资源。每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLoal方法:

void set(T value)
T get()
void remove()
protected T initialValue()

ThreadLocal底层实现:

Map(叫做ThreadLocalMap ),每一个元素的 key 值为线程对象,而值对应线程的变量副本 。当调用 get、set 方法时,使用的线程是当前在运行的线程,从而可以获取、设置当前在运行的线程的私有变量。

2. Synchronize

关键字synchronize拥有锁重入的功能,也就是在使用synchronize时,当一个线程的得到了一个对象的锁后,再次请求此对象是可以再次得到该对象的锁。
当一个线程请求一个由其他线程持有的锁时,发出请求的线程就会被阻塞,然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由她自己持有的锁,那么这个请求就会成功,“重入” 意味着获取锁的 操作的粒度是“线程”,而不是调用。

Synchronize和Lock的区别:
synchronized的局限性
占有锁的线程等待IO或者其他原因被阻塞,没有释放锁的情况下,其他线程一直阻塞
多个线程同时读写文件的时候,读和读操作也会发生冲突
我们没有办法知道当前我们的线程是否成功获取了锁,只能傻傻的等待

线程通信、进程通信

线程通信的几种方式:

  1. 共享变量
  2. wait/notify机制
  3. Lock/Condition机制
  4. 管道

进程通信:

final, finally, finalize 的区别
final

修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能重载。

finally

在异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。

finalize

方法名。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。

int 和 Integer 有什么区别

int 是基本数据类型
Integer是其包装类,注意是一个类。
为什么要提供包装类呢???
一是为了在各种类型间转化,通过各种方法的调用。否则 你无法直接通过变量转化。
比如,现在int要转为String

int a=0;
String result=Integer.toString(a);
在java中包装类,比较多的用途是用在于各种数据类型的转化中。
我写几个demo
//通过包装类来实现转化的

int num=Integer.valueOf(“12”);
int num2=Integer.parseInt(“12”);
double num3=Double.valueOf(“12.2”);
double num4=Double.parseDouble(“12.2”);
//其他的类似。通过基本数据类型的包装来的valueOf和parseXX来实现String转为XX
String a=String.valueOf(“1234”);//这里括号中几乎可以是任何类型
String b=String.valueOf(true);
String c=new Integer(12).toString();//通过包装类的toString()也可以
String d=new Double(2.3).toString();
再举例下。比如我现在要用泛型

List nums;
这里<>需要类。如果你用int。它会报错的。

重载和重写的区别

override(重写)

  1. 方法名、参数、返回值相同。
  2. 子类方法不能缩小父类方法的访问权限。
  3. 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
  4. 存在于父类和子类之间。
  5. 方法被定义为final不能被重写。

overload(重载)

  1. 参数类型、个数、顺序至少有一个不相同。
  2. 不能重载只有返回值不同的方法名。
  3. 存在于父类和子类、同类中。
HTTP 请求的 GET 与 POST 方式的区别
  • GET方法会把名值对追加在请求的URL后面。因为URL对字符数目有限制,进而限制了用在客户端请求的参数值的数目。并且请求中的参数值是可见的,因此,敏感信息不能用这种方式传递。
  • POST方法通过把请求参数值放在请求体中来克服GET方法的限制,因此,可以发送的参数的数目是没有限制的。最后,通过POST请求传递的敏感信息对外部客户端是不可见的。
session 与 cookie 区别
  • cookie: 是 Web 服务器发送给浏览器的一块信息。浏览器会在本地文件中给每一个 Web 服务
    器存储 cookie。以后浏览器在给特定的 Web 服务器发请求的时候,同时会发送所有为该服
    务器存储的 cookie。
  • session:
    下面列出了 session 和 cookie 的区别:
  1. Cookie和Session都是会话技术,Cookie是运行在客户端,Session是运行在服务器端。
  2. Cookie有大小限制以及浏览器在存cookie的个数也有限制,Session是没有大小限制和服务器的内存大小有关。
  3. Cookie有安全隐患,通过拦截或本地文件找得到你的cookie后可以进行攻击。
  4. Session是保存在服务器端上会存在一段时间才会消失,如果session过多会增加服务器的压力。
  5. 无论客户端浏览器做怎么样的设置,session都应该能正常工作。客户端可以选择禁用 cookie,但是, session 仍然是能够工作的,因为客户端无法禁用服务端的 session。
JDBC 流程

1、 加载JDBC驱动程序:
在连接数据库之前,首先要加载想要连接的数据库的驱动到JVM(Java虚拟机),
这通过java.lang.Class类的静态方法forName(String className)实现。
例如:

try{
//加载MySql的驱动类
Class.forName(“com.mysql.jdbc.Driver”) ;
}catch(ClassNotFoundException e){
System.out.println(“找不到驱动程序类 ,加载驱动失败!”);
e.printStackTrace() ;
}
成功加载后,会将Driver类的实例注册到DriverManager类中。

2、 提供JDBC连接的URL

连接URL定义了连接数据库时的协议、子协议、数据源标识。
书写形式:协议:子协议:数据源标识
协议:在JDBC中总是以jdbc开始 子协议:是桥连接的驱动程序或是数据库管理系统名称。
数据源标识:标记找到数据库来源的地址与连接端口。
例如:
jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=gbk;useUnicode=true;(MySql的连接URL)
表示使用Unicode字符集。如果characterEncoding设置为 gb2312或GBK,本参数必须设置为true 。characterEncoding=gbk:字符编码方式。

3、创建数据库的连接

要连接数据库,需要向java.sql.DriverManager请求并获得Connection对象, 该对象就代表一个数据库的连接。
使用DriverManager的getConnectin(String url , String username , String password )方法传入指定的欲连接的数据库的路径、数据库的用户名和 密码来获得。
例如: //连接MySql数据库,用户名和密码都是root

String url = "jdbc:mysql://localhost:3306/test" ;
String username = "root" ;
String password = "root" ;
try{
Connection con = DriverManager.getConnection(url , username , password ) ;
}catch(SQLException se){
System.out.println("数据库连接失败!");
se.printStackTrace() ;
}

4、 创建一个Statement
•要执行SQL语句,必须获得java.sql.Statement实例,Statement实例分为以下3 种类型:
1、执行静态SQL语句。通常通过Statement实例实现。
2、执行动态SQL语句。通常通过PreparedStatement实例实现。
3、执行数据库存储过程。通常通过CallableStatement实例实现。
具体的实现方式:

Statement stmt = con.createStatement() ; PreparedStatement pstmt = con.prepareStatement(sql) ; CallableStatement cstmt = con.prepareCall(“{CALL demoSp(? , ?)}”) ;

5、执行SQL语句
Statement接口提供了三种执行SQL语句的方法:executeQuery 、executeUpdate 和execute
1、ResultSet executeQuery(String sqlString):执行查询数据库的SQL语句 ,返回一个结果集(ResultSet)对象。
2、int executeUpdate(String sqlString):用于执行INSERT、UPDATE或 DELETE语句以及SQL DDL语句,如:CREATE TABLE和DROP TABLE等
3、execute(sqlString):用于执行返回多个结果集、多个更新计数或二者组合的 语句。 具体实现的代码:

ResultSet rs = stmt.executeQuery(“SELECT * FROM …”) ; int rows = stmt.executeUpdate(“INSERT INTO …”) ; boolean flag = stmt.execute(String sql) ;

6、处理结果
两种情况:
1、执行更新返回的是本次操作影响到的记录数。
2、执行查询返回的结果是一个ResultSet对象。
• ResultSet包含符合SQL语句中条件的所有行,并且它通过一套get方法提供了对这些 行中数据的访问。
• 使用结果集(ResultSet)对象的访问方法获取数据:

while(rs.next()){
String name = rs.getString(“name”) ;
String pass = rs.getString(1) ; // 此方法比较高效
}
(列是从左到右编号的,并且从列1开始)

7、关闭JDBC对象
操作完成以后要把所有使用的JDBC对象全都关闭,以释放JDBC资源,关闭顺序和声 明顺序相反:
1、关闭记录集
2、关闭声明
3、关闭连接对象

if(rs != null){ // 关闭记录集
    try{
      rs.close() ;
    }catch(SQLException e){
      e.printStackTrace() ;
    }
}
if(stmt != null){ // 关闭声明
    try{
      stmt.close() ;
    }catch(SQLException e){
      e.printStackTrace() ;
    }
}
if(conn != null){ // 关闭连接对象
    try{
      conn.close() ;
    }catch(SQLException e){
      e.printStackTrace() ;
    }
}

集合

Arraylist 与 LinkedList 区别

Arraylist:

优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。

缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。

LinkedList:

优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景

缺点:因为LinkedList要移动指针,所以查询操作性能比较低。

适用场景分析:

当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。

HashMap 和 Hashtable 的区别

1.hashMap去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法。

2.hashTable同步的,而HashMap是非同步的,效率上逼hashTable要高。

3.hashMap允许空键值,而hashTable不允许。

HashMap 和 ConcurrentHashMap 的区别

ConcurrentHashMap是线程安全的HashMap的实现。

(1)ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

(2)HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

数据库

需要掌握的几个点:

  1. 数据库索引原理/复合索引
  2. 数据库三大范式
  3. 数据库事物(四大特性)
  4. 数据库优化
数据库事物
  • 原子性
  • 一致性
  • 隔离性
  • 持久性

说说 SQL 优化之道
sql优化的几种方法

数据库优化/提高数据库性能的方法
  1. 排序
  2. 索引
  3. 连续的磁盘存储
  4. 分类、聚簇 把关系非常紧密,但位于不同表中的行记录,在磁盘中临近存储,当他们做连接运算时,就能迅速得到结果。
  5. 内存缓冲
  6. 并发执行
  7. 查询优化
  8. 日志和数据分盘存储

查询优化和并发执行完全封装在DBMS中,对数据库设计者和DBA透明。

其他的方法都需要数据库设计者和DBA进行配置。

查询优化
  1. 对查询进行优化,应尽量避免全表扫描,首先应考虑在where及order by设计的列上简历索引。
  2. where避免null判断(浪费索引,导致全表扫描)
  3. where避免使用!=或<>(浪费索引,导致全表扫描)
  4. where避免使用or,使用union来代替(浪费索引,导致全表扫描)
  5. in或not in慎用(导致全表扫描)
  6. %k%
  7. where避免使用参数,导致全表扫描
  8. where避免使用表达式操作
  9. where避免函数操作
  10. where避免在子句中的”=“左边进行函数或其他表达式运算
  11. 复合索引
  12. exists代替in
  13. 尽量使用varchar节省空间
  14. 不使用*号,而应该使用具体的字段列表
  15. 尽量避免大事务操作,提高系统并发能力
  16. 避免频繁使用临时表,以减少系统表资源的消耗
哈希索引适合于什么特性的表?不适合于什么特性的表?

哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。
在Mysql中,只有Memory引擎显式支持哈希索引。

不支持任何范围查询,只支持等值比较查询
乱序

MySQL千万或者上亿的数据怎么设计数据库

垂直分表(根据业务)/水平分表(根据某个字段)

实际上,水平分表现在最流行的实现方式,是通过水平分库来实现的。即刚才所说通过id的0-9末尾分成的10个表,分布在10个mysql数据库上。这样可以通过多个低配置主机整合起来,实现高性能。

数据库

为什么要用 B-tree
鉴于B-tree具有良好的定位特性,其常被用于对检索时间要求苛刻的场合,例如:
1、B-tree索引是数据库中存取和查找文件(称为记录或键值)的一种方法。
2、硬盘中的结点也是B-tree结构的。与内存相比,硬盘必须花成倍的时间来存取一个数据元素,这是因为硬盘的机械部件读写数据的速度远远赶不上纯电子媒体的内存。与一个结点两个分支的二元树相比,B-tree利用多个分支(称为子树)的结点,减少获取记录时所经历的结点数,从而达到节省存取时间的目的。

Java Web和框架

Java框架发展

1. 纯Servlet开发(Service+Applet)

在servlet中可以通过挨着行输出HTML语句来实现页面的样式和输出,表现、逻辑、控制、业务全部混在Servlet类中,最多把模型层单独写出来。

2. 纯JSP开发(Java Server Page)

HTML页面中可以在<% %>添加Java代码。但表现、控制、模型、业务逻辑,依然全部混在JSP中。

3. Model1开发模式(JSP+JavaBean)

JavaBean:JavaBean是一个遵循特定写法的Java类,这个类必须有一个无参的构造函数,属性必须私有化,但私有化的属性必须通过public类型的方法(getter & setter)暴露给其他程,并且方法的命名也必须遵守一定的命名规范。

在JavaEE开发中,JavaBean通常用来封装数据,对于遵循以上写法的JavaBean组件,其它程序可以通过反射技术实例化JavaBean对象,并且通过反射那些遵守命名规范的方法,从而获知JavaBean的属性,进而调用其属性保存数据。

即把模型层分离出来了,JavaBean负责了少数的业务逻辑(比如读写数据库)。但JSP仍然需要负责显示、多数业务逻辑、控制页面跳转。

4. Model2开发模式(JSP+JavaBean+Servlet)

MVC开发模式初体现

  • JavaBean 模型层 --包含持久层、业务逻辑层、模型层
  • JSP 视图层
  • Servlet 控制层

模型层可以继续分层:dao持久层、service业务逻辑层、entity实体类、util工具包…

servlet

Servlet(Server Applet)是Java servlet的简称,称谓小服务程序或服务连接器,用Java编写的服务端程序,主要用于交互式地浏览和修改数据,生成动态web内容。

狭义的servlet是指Java语言实现的一个借口,钢轮车的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。Servlet运行与支持Java的应用服务器中。从原理上讲,servlet可以相应任何类型的请求,但绝大多数情况下Servlet指用来扩展基于HTTP协议的Web服务器。

Tomcat和servlet的关系:

Tomcat 是Web应用服务器,是一个Servlet/JSP容器. Tomcat作为Servlet容器,负责处理客户请求,把请求传送给Servlet,并将Servlet的响应传送回给客户.

而Servlet是一种运行在支持Java语言的服务器上的组件. Servlet最常见的用途是扩展Java Web服务器功能,提供非常安全的,可移植的,易于使用的CGI替代品.

Servlet的生命周期

服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfigconf),该servlet对象去处理所有客户端请求,在service(ServletRequest req,ServletResponseres)方法中执行,最后服务器关闭时,才会销毁这个servlet对象,执行destroy()方法。

init() -> server() -> destroy()

Servlet的运行过程

Servlet程序是由WEB服务器调用,web服务器收到客户端的Servlet访问请求后:

a. Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行d,否则,执行b。
b. 装载并创建该Servlet的一个实例对象。
c. 调用Servlet实例对象的init()方法。

d. 创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
e. WEB应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。

创建一个简单的Servlet
public class TestServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("post");
    }
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("post");
    }
}

为什么创建Servlet不直接继承Servlet接口,而是要继承HttpServlet
HTTPServlet继承自GenericServlet,而GenericServlet实现了Servlet和ServletConfig接口,实现了Servlet的方法,简化了编写Servlet的步骤。

为什么不重写Servlet的server()方法,而是重写doGet和doPost方法?
对于HTTPServlet的Server()方法,作用是判断浏览器过来的请求方式,并执行不同的处理方式(5种),servlet将不同的方法提取了出来,而我们最常用的是get和post方法,所以只需要重写get和post方法就可以了。

Servlet执行流程

Spring

Spring是一个轻型容器(light-weight container),其核心是Bean工厂(Bean Factory),用以构造我们所需要的M(Model)。在此基础之上,Spring提供了AOP(Aspect-Oriented
Programming, 面向层面的编程)的实现,用它来提供非管理环境下申明方式的事务、安全等服务;对Bean工厂的扩展ApplicationContext更加方便我们实现J2EE的应用;DAO/ORM的实现方便我们进行数据库的开发;Web MVC和Spring Web提供了Java Web应用的框架或与其他流行的Web框架进行集成。

MVC设计思想

MVC就是
M:Model 模型
V:View 视图
C:Controller 控制器
模型就是封装业务逻辑和数据的一个一个的模块,控制器就是调用这些模块的(java中通常是用Servlet来实现,框架的话很多是用Struts2来实现这一层),视图就主要是你看到的,比如JSP等.
当用户发出请求的时候,控制器根据请求来选择要处理的业务逻辑和要选择的数据,再返回去把结果输出到视图层,这里可能是进行重定向或转发等.

MVC优缺点

(一)优点

(二)缺点

  1. 增加系统结构和实现的复杂性
  2. 视图和控制器间的过于紧密的连接。
SpringMVC的执行流程
AOP面向切面编程/AOP实现原理

OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP实现原理

AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。

AspectJ是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

public interface Person {
    String sayHello();
}

@Component
public class Man implements Person {
    @Timer0
    @Override
    public String sayHello();
}

// 定义Aspect
@Aspect
@Component
public class AdviceTest {
    @Pointcut("@annotation(com.***.**.Timer)")  // 定义切点,正则表达式的意思是在执行**类的**方法时,将会发送performance切入点通知
    public void pointcut() {}
    
    @Before("pointcut") // 执行之前
    public void before() {}
    
    @After("pointcut")  // 执行之后
    public void doAfter(){}
    
    @AfterReturning("poiintcut") // 所拦截方法return后,执行该注解的函数
    
    @Around("pointcut") // 同时在所拦截方法的前后执行一段逻辑
}

以上注解的有限
使用Spring AOP统一处理Http请求

  1. 添加依赖sprint-statrer-aop
  2. aspect层HttpAspect类
@Aspect
@Component  // 把类引入到容器
public class HttpAspect {
    private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class);

    @Pointcut("execution(public * com.**.controller.GirlController.*(..))") //切面
    public void log() {
    }
    
    @Before("log()")
    public void doAfter() {
        logger.info("1111");
    }
    
    @After("log()")
    public void doAfter() {
        logger.info();
    }
}
AOP源码分析
IOC控制反转

控制反转也叫依赖注入,IOC利用java反射机制,AOP利用代理模式。所谓控制反转是指,本来被调用者的实例是有调用者来创建的,这样的缺点是耦合性太强,IOC则是统一交给spring来管理创建,将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关的属性,让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类。

BeanFactory 和 ApplicationContext 有什么区别
beanfactory顾名思义,它的核心概念就是bean工厂,用作于bean生命周期的管理,而applicationcontext这个概念就比较丰富了,单看名字(应用上下文)就能看出它包含的范围更广,它继承自bean factory但不仅仅是继承自这一个接口,还有继承了其他的接口,所以它不仅仅有bean factory相关概念,更是一个应用系统的上下文,其设计初衷应该是一个包罗万象的对外暴露的一个综合的API。

设计模式

手写单例模式
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值