ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java 工程师核心基础修炼

2019-07-02 08:55:40  阅读:209  来源: 互联网

标签:Java 工程师 对象 public int 修炼 线程 Class


金三银四,目前正值招聘求职旺季,最近也面试了不少应聘 Java 工程师的同学,很多同学对一些新技术名词都能侃侃而谈,但对一些核心原理理解的不够透彻,特别是对 Java 的一些核心基础知识点掌握的不够,例如 JVM、常用的算法和数据结构等。正所谓万丈高楼平地起,只有把基础掌握的牢固,才能走的更远,面对不断更新的技术才能快速掌握,同时在面试、工作中也更能脱颖而出!

想在面试、工作中脱颖而出?想在最短的时间内快速掌握 Java 的核心基础知识点?想要成为一位优秀的 Java 工程师?本篇文章能助你一臂之力!

本 Chat 主要包括以下内容:

  1. Java 核心基础:Java 异常分类及处理、Java 反射、Java 注解、Java 泛型、Java 序列化、Java 复制等。
  2. JVM 知识点:JVM 内存区域、JVM 运行时内存、垃圾回收与算法等。
  3. Java 多线程并发:Java 并发知识、Java 线程实现创建、线程锁等。
  4. Java 常用算法、数据结构。

1 Java 的异常分类及处理

1.1 异常分类

Throwable 是 Java 语言中所有错误或异常的超类。下一层分为 ErrorExceptionError 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。

Exception 又有两个分支,一个是运行时异常 RuntimeException ,如:NullPointerException 、 ClassCastException;一个是检查CheckedException,如 I/O 错误导致的 IOException、SQLException。

RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。 如果出现 RuntimeException,那么一定是程序员的错误。

CheckedException 一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:

  • 试图在文件尾部读取数据;
  • 试图打开一个错误格式的 URL;
  • 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在。

1.2 异常处理方式

抛出异常有三种形式,一个是 throw,一个是 throws,还有一种系统自动抛异常。如:

public static void main(String[] args) {         String s = "abc";         if(s.equals("abc")) {            throw new NumberFormatException();        } else {             System.out.println(s);              }        } int div(int a,int b) throws Exception{     return a/b; }      

2 Java 反射

2.1 动态语言

动态语言,是指程序在运行时可以改变其结构:新的函数可以引进,已有的函数可以被删除等结构上的变化。比如常见的 JavaScript 就是动态语言,除此之外 Ruby、Python 等也属于动态语言,而 C、C++ 则不属于动态语言。从反射角度说 Java 属于半动态语言。

2.2 反射机制概念

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

2.3 反射的应用场合

编译时类型和运行时类型

在 Java 程序中许多对象在运行是都会出现两种类型:编译时类型和运行时类型。 编译时的类型由声明对象时实用的类型来决定,运行时的类型由实际赋值给对象的类型决定 。如:

    Person p=new Student();

其中编译时类型为 Person,运行时类型为 Student。

编译时类型无法获取具体方法

程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为 Object,但是程序有需要调用该对象的运行时类型的方法。为了解决这些问题,程序需要在运行时发现对象和类的真实信息。然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,此时就必须使用到反射了。

2.4 Java 反射 API

反射 API 用来生成 JVM 中的类、接口或对象的信息

  • Class 类:反射的核心类,可以获取类的属性,方法等信息。
  • Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
  • Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
  • Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。

2.5 反射使用步骤

(1)获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法。(2)调用 Class 类中的方法,既就是反射的使用阶段。(3)使用反射 API 来操作这些信息。

获取 Class 对象的三种方法

调用某个对象的 getClass()方法

Person p=new Person();Class clazz=p.getClass();

调用某个类的 class 属性来获取该类对应的 Class 对象

Class clazz=Person.class;

使用 Class 类中的 forName()静态方法(最安全/性能最好)

 Class clazz=Class.forName("类的全路径"); (最常用)

当我们获得了想要操作的类的 Class 对象后,可以通过 Class 类中的方法获取并查看该类中的方法和属性。

 //获取 Person 类的 Class 对象    Class clazz=Class.forName("reflection.Person");    //获取 Person 类的所有方法信息     Method[] method=clazz.getDeclaredMethods();     for(Method m:method){        System.out.println(m.toString());    }     //获取 Person 类的所有成员属性信息     Field[] field=clazz.getDeclaredFields();     for(Field f:field){        System.out.println(f.toString());     }    //获取 Person 类的所有构造方法信息     Constructor[] constructor=clazz.getDeclaredConstructors();     for(Constructor c:constructor){        System.out.println(c.toString());    }

创建对象的两种方法

(1)使用 Class 对象的 newInstance() 方法来创建该 Class 对象对应类的实例,但是这种方法要求该 Class 对象对应的类有默认的空构造器。

(2)先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance()方法来创建 Class 对象对应类的实例,通过这种方法可以选定构造方法创建实例。

   //获取 Person 类的 Class 对象    Class clazz=Class.forName("reflection.Person");     //使用.newInstane 方法创建对象     Person p=(Person) clazz.newInstance();     //获取构造方法并创建对象    Constructor    c=clazz.getDeclaredConstructor(String.class,String.class,int.class);     //创建对象并设置属性    Person p1=(Person) c.newInstance("张三","男",20);

3 Java 注解

Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。

3.1 四种标注元注解

元注解的作用是负责注解其他注解。 Java5.0 定义了 4 个标准的 meta-annotation 类型,它们被用来提供对其它 annotation 类型作说明。

  • @Target 修饰的对象范围

@Target 说明了Annotation所修饰的对象范围, Annotation 可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target 可更加明晰其修饰的目标。

  • @Retention 定义被保留的时间长短

@Retention 定义了该 Annotation 被保留的时间长短,表示需要在什么级别保存注解信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)有:

SOURCE:在源文件中有效(即源文件保留)

CLASS:在 class 文件中有效(即 class 保留)

RUNTIME:在运行时有效(即运行时保留)

  • @Documented 描述 -javadoc

@Documented 用于描述其它类型的 Annotation 应该作为被标注程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。

  • @Inherited 阐述了某个被标注的类型是被继承的

@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 Annotation 类型被用于一个 Class,则这个 Annotation 将被用于该 Class 的子类。

3.2 注解处理器

如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注解处理器。Java SE5 扩展了反射机制的 API,以帮助程序员快速的构造自定义注解处理器。下面实现一个注解处理器。

  //定义注解    @Target(ElementType.FIELD)    @Retention(RetentionPolicy.RUNTIME)    @Documented    public @interface FruitProvider {    /**供应商编号*/     public int id() default -1;    /**供应商名称*/    public String name() default "";    /**供应商地址*/    public String address() default "";     }    //注解使用    public class Apple {    @FruitProvider(id = 1, name = "XX公司", address = "XX  路")     private String appleProvider;     public void setAppleProvider(String appleProvider){        this.appleProvider = appleProvider;       }     public String getAppleProvider() {         return appleProvider;            }     }    //注解处理器    public class FruitInfoUtil {    public static void getFruitInfo(Class clazz) {     Field[] fields = clazz.getDeclaredFields();       for (Field field : fields) {          if(field.isAnnotationPresent(FruitProvider.class))   { FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);      //注解信息的处理地方     strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"+ fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();  System.out.println(strFruitProvicer); } } } }

4 Java 泛型

泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。比如我们要写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,我们就可以使用 Java 泛型。

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

public class Box {       private T t;        public void add(T t) {        this.t = t;     }     public T get() {       return t;     }

5 Java 复制

将一个对象的引用复制给另外一个对象,一共有三种方式。第一种方式是直接赋值,第二种方式是浅拷贝,第三种是深拷贝。所以大家知道了哈,这三种方式实际上都是为了拷贝对象。

5.1 直接赋值复制

在 Java 中,A a1 = a2,我们需要理解的是这实际上复制的是引用,也就是说 a1 和 a2 指向的是同一个对象。因此,当 a1 变化的时候,a2 里面的成员变量也会跟着变化。

5.2 浅复制(复制引用但不复制引用的对象)

创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

   Class Resume implements Cloneable{         public Object clone() {           try { return (Resume)super.clone();          } catch (Exception e) {               e.printStackTrace();               return null;            }         }      }

5.3 深复制(复制对象和其应用对象)

深拷贝不仅复制对象本身,而且复制对象包含的引用指向的所有对象。

  class Student implements Cloneable {        String name;         int age;         Professor p;         Student(String name, int age, Professor p) {             this.name = name;             this.age = age;             this.p = p;     }         public Object clone() {             Student o = null;             try {                 o = (Student) super.clone();             } catch (CloneNotSupportedException e) {                 System.out.println(e.toString());             }             o.p = (Professor) p.clone();            return o;         }     }

6 JVM

JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。

6.1 运行过程

我们都知道 Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。也就是如下:

① Java 源文件—->编译器—->字节码文件

② 字节码文件—->JVM—->机器码

每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

6.2 JVM 内存区域

JVM 内存区域主要分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(JAVA 堆、方法区)、直接内存。

线程私有数据区域生命周期与线程相同,依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存/否跟随本地线程的生/死对应)。

线程共享区域随虚拟机的启动/关闭而创建/销毁。

直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作,这样就避免了在 Java堆和 Native 堆中来回复制数据,因此在一些场景中可以显著提高性能。

6.3 JVM 运行时内存

Java 堆从 GC 的角度还可以细分为:新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

  • 新生代

是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

ServivorTo:保留了一次 MinorGC 过程中的幸存者。

  • 老年代

主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

6.4 垃圾回收与算法

  • 如何确定垃圾

引用计数法:在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。

可达性分析:为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

  • 垃圾回收算法

标记清除算法(Mark-Sweep)最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

复制算法(copying)为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。

分代收集算法分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

  • 新生代与复制算法

目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。

  • 老年代与标记复制算法

老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。

(1)Java虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。

(2)对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。

(3)当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。

(4)如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。

(5)在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。

(6)当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。

7 Java 多线程并发

7.1 Java 线程实现/创建方式

继承 Thread 类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

 public class MyThread extends Thread {          public void run() {              System.out.println("MyThread.run()");          }     }         MyThread myThread1 = new MyThread();         myThread1.start();

实现 Runnable 接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。

public class MyThread extends OtherClass implements Runnable {         public void run() {              System.out.println("MyThread.run()");          }     }     //启动 MyThread    MyThread myThread = new MyThread();     Thread thread = new Thread(myThread);     thread.start();     target.run()    public void run() {      if (target != null) {      target.run();      }     }

ExecutorService、Callable、Future 有返回值线程

有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

//创建一个线程池    ExecutorService pool = Executors.newFixedThreadPool(taskSize);    // 创建多个有返回值的任务    List<Future> list = new ArrayList<Future>();     for (int i = 0; i < taskSize; i++) {     Callable c = new MyCallable(i + " ");     // 执行任务并获取 Future 对象    Future f = pool.submit(c);     list.add(f);     }     // 关闭线程池    pool.shutdown();     // 获取所有并发任务的运行结果    for (Future f : list) {     // 从 Future 对象上获取任务的返回值,并输出到控制台    System.out.println("res:" + f.get().toString());     }

基于线程池的方式

线程和数据库连接这些资源都是非常宝贵的资源。如果每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

   // 创建线程池    ExecutorService threadPool = Executors.newFixedThreadPool(10);     while(true) {     threadPool.execute(new Runnable() { // 提交多个线程执行     @Override     public void run() {             System.out.println(Thread.currentThread().getName() + " is running ..");     try {         Thread.sleep(3000);     } catch (InterruptedException e) {         e.printStackTrace();             }           }        });      }    }

7.2 同步锁与死锁

同步锁当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。

死锁何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

7.3 线程池原理

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。主要特点为:线程复用;控制最大并发数;管理线程。

线程复用一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

线程池的组成一般的线程池主要分为以下 4 个组成部分:

(1)线程池管理器:用于创建并管理线程池。(2)工作线程:线程池中的线程。(3)任务接口:每个任务必须实现的接口,用于工作线程调度其运行。(4)任务队列:用于存放待处理的任务,提供一种缓冲机制。

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。

ThreadPoolExecutor 的构造方法如下:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime,            TimeUnit unit, BlockingQueue<Runnable> workQueue) {            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);    }

corePoolSize:指定了线程池中的线程数量。

maximumPoolSize:指定了线程池中的最大线程数量。

keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。

unit:keepAliveTime 的单位。

workQueue:任务队列,被提交但尚未被执行的任务。

threadFactory:线程工厂,用于创建线程,一般用默认的即可。

handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

拒绝策略线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

JDK 内置的拒绝策略如下:

AbortPolicy : 直接抛出异常,阻止系统正常运行。

CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。

Java 线程池工作过程(1)线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

(2)当调用 execute() 方法添加一个任务时,线程池会做如下判断:

a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;c) 如果这时候队列满了,而且正在运行的线程数量小maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;d) 如果队列满了,而且正在运行的线程数量大于或等maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。

(3)当一个线程完成任务时,它会从队列中取下一个任务来执行。

(4)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

8 Java 常用算法

8.1 快速排序算法

快速排序的原理:选择一个关键值作为基准值。比基准值小的都在左边序列(一般是无序的),比基准值大的都在右边(一般是无序的)。一般选择序列的第一个元素。一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。直到从前往后的比较索引>从后往前比较的索引,结束第一次循环,此时,对于基准值来说,左右两边就是有序的了。

  public void sort(int[] a,int low,int high){         int start = low;         int end = high;         int key = a[low];          while(end>start){         //从后往前比较         while(end>start&&a[end]>=key)         //如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较         end--;         if(a[end]<=key){             int temp = a[end];             a[end] = a[start];             a[start] = temp;         }         //从前往后比较         while(end>start&&a[start]<=key)        //如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置         start++;         if(a[start]>=key){             int temp = a[start];             a[start] = a[end];             a[end] = temp;         }         //此时第一次循环比较结束,关键值的位置已经确定了。左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用     }         //递归        if(start>low) sort(a,low,start-1);//左边序列。第一个索引位置到关键值索引-1         if(end<high) sort(a,end+1,high);//右边序列。从关键值索引+1 到最后一个         } }

8.2 冒泡排序算法

(1)比较前后相邻的二个数据,如果前面数据大于后面的数据,就将这二个数据交换。

(2)这样对数组的第 0 个数据到 N-1 个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1 个位置。

(3)N=N-1,如果 N 不为 0 就重复前面二步,否则排序完成。

public static void bubbleSort1(int [] a, int n){         int i, j;         for(i=0; i<n; i++){//表示 n 次排序过程。             for(j=1; j<n-i; j++){                 if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换                //交换 a[j-1]和 a[j]                int temp;                temp = a[j-1];                a[j-1] = a[j];                a[j]=temp;                }            }         }    }

8.3 二分查找

又叫折半查找,要求待查找的序列有序。每次取中间位置的值与待查关键字比较,如果中间位置的值比待查关键字大,则在前半部分循环这个查找的过程,如果中间位置的值比待查关键字小,则在后半部分循环这个查找的过程。直到查找到了为止,否则序列中没有待查的关键字。

public static int biSearch(int []array,int a){         int lo=0;         int hi=array.length-1;         int mid;         while(lo<=hi){             mid=(lo+hi)/2;//中间位置             if(array[mid]==a){             return mid+1;             }else if(array[mid]<a){ //向右查找                 lo=mid+1;             }else{ //向左查找                 hi=mid-1;             }         }         return -1;    }

8.4 基数排序算法

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

 public class radixSort {        inta[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,101,56,17,18,23,34,15,35,2    5,53,51};    public radixSort(){        sort(a);        for(inti=0;i<a.length;i++){            System.out.println(a[i]);        }    }    public void sort(int[] array){        //首先确定排序的趟数;        int max=array[0];        for(inti=1;i<array.length;i++){            if(array[i]>max){                max=array[i];        }    }        int time=0;        //判断位数;        while(max>0){            max/=10;            time++;        }        //建立 10 个队列;        List<ArrayList> queue=newArrayList<ArrayList>();        for(int i=0;i<10;i++){        ArrayList<Integer>queue1=new ArrayList<Integer>();        queue.add(queue1);        }        //进行 time 次分配和收集;        for(int i=0;i<time;i++){        //分配数组元素;        for(intj=0;j<array.length;j++){        //得到数字的第 time+1 位数;        int x=array[j]%(int)Math.pow(10,i+1)/(int)Math.pow(10, i);        ArrayList<Integer>queue2=queue.get(x);        queue2.add(array[j]);        queue.set(x, queue2);        }        int count=0;//元素计数器;        //收集队列元素;        for(int k=0;k<10;k++){            while(queue.get(k).size()>0){                ArrayList<Integer>queue3=queue.get(k);                array[count]=queue3.get(0);                queue3.remove(0);                count++;        }    }    }    }    }

8.5 插入排序算法

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应的位置并插入。插入排序非常类似于整扑克牌。在开始摸牌时,左手是空的,牌面朝下放在桌上。接着,一次从桌上摸起一张牌,并将它插入到左手一把牌中的正确位置上。为了找到这张牌的正确位置,要将它与手中已有的牌从右到左地进行比较。无论什么时候,左手中的牌都是排好序的。如果输入数组已经是排好序的话,插入排序出现最佳情况,其运行时间是输入规模的一个线性函数。如果输入数组是逆序排列的,将出现最坏情况。平均情况与最坏情况一样,其时间代价是(n2)。

  public void sort(int arr[]){        for(int i =1; i<arr.length;i++)     {     //插入的数     int insertVal = arr[i];     //被插入的位置(准备和前一个数比较)     int index = i-1;     //如果插入的数比被插入的数小     while(index>=0&&insertVal<arr[index])     {     //将把 arr[index] 向后移动     arr[index+1]=arr[index];     //让 index 向前移动     index--;     }     //把插入的数放入合适位置     arr[index+1]=insertVal;       }     }

9 数据结构

  • 栈(stack)

栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈顶(top)。它是后进先出(LIFO)的。对栈的基本操作只有 push(进栈)和 pop(出栈)两种,前者相当于插入,后者相当于删除最后的元素。

  • 队列(queue)

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。

  • 链表(Link)

表是一种数据结构,和数组同级。比如,Java 中我们使用的 ArrayList,其实现原理是数组。而LinkedList 的实现原理就是链表了。链表在进行循环遍历时效率不高,但是插入和删除时优势明显。

  • 散列表(Hash Table)

散列表(Hash table,也叫哈希表)是一种查找算法,与链表、树等算法不同的是,散列表算法在查找时不需要进行一系列和关键字(关键字是数据元素中某个数据项的值,用以标识一个数据元素)的比较操作。散列表算法希望能尽量做到不经过任何比较,通过一次存取就能得到所查找的数据元素,因而必须要在数据元素的存储位置和它的关键字(可用 key 表示)之间建立一个确定的对应关系,使每个关键字和散列表中一个唯一的存储位置相对应。因此在查找时,只要根据这个对应关系找到给定关键字在散列表中的位置即可。这种对应关系被称为散列函数(可用 h(key)表示)。

用的构造散列函数的方法有:

(1)直接定址法: 取关键字或关键字的某个线性函数值为散列地址,即:h(key) = key 或 h(key) = a * key + b,其中 a 和 b 为常数。(2)数字分析法(3)方取值法: 取关键字平方后的中间几位为散列地址。(4)折叠法:将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为散列地址。(5)除留余数法:取关键字被某个不大于散列表表长 m 的数 p 除后所得的余数为散列地址,即:h(key) = key MOD p p ≤ m(6)随机数法:选择一个随机函数,取关键字的随机函数值为它的散列地址,即:h(key) = random(key)

10 结束语

本 Chat 涉及的知识点比较多,范围广,由于时间仓促,很多都未能展开详细的论述,出错难免,如发现错漏,敬请各位同学在读者圈提出。也希望本 Chat 能对各位同学学习 Java 的核心基础知识有所帮助,使大家在工作或面试中能融汇贯通,形成完整的知识体系。谢谢各位同学的支持!


本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

阅读全文: http://gitbook.cn/gitchat/activity/5cb2af63da8b092c221c84cc

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

标签:Java,工程师,对象,public,int,修炼,线程,Class
来源: https://blog.csdn.net/valada/article/details/94420877

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有