面试_Java基础

目录

Java基础

类加载过程,类加载器有哪些

jdk自带有三个类加载器:bootstrap classloader、extclassloader、appclassloader

  • bootstrap classloader是extclassloader的父类加载器,默认负责加载java_home/lib下的jar包h和class文件。
  • extclassloader是appclassloader的父类加载器,负责加载java_home/lib/ext下的jar包h和class文件。
  • appclassloader是自定义类加载器的父类,负责加载classpath下的类文件
  • 继承classloader实现自定义类加载器。

双亲委托模型
在这里插入图片描述
向上委派:就是一层一层查找缓存,当查找到bootstrap中还没有缓存时,就开始向下查找加载路径,如果还是找不到就会报错,找不到类。
向下查找:查找加载路径。
双亲委托模型的好处:

  • 主要是为了安全性,自己编写的类动态替换Java的一些核心类,比如String。
  • 同时也避免了类的重复加载,因为jvm区分不同类,不仅仅是根据类名,相同的class文件被不同的classloader加载就是两个不同的类。

并发容器问题

Java内存模型是怎样的

什么是字节码,采用字节码有什么好处

  • java编译器/解释器:在windows和java程序之间存在一个jvm,jvm在任何平台上提供一个共同的接口。编译程序时只需要面向虚拟机,生成虚拟机能够理解的代码,java.clas(字节码),然后由解释器将字节码转换成机器码。
  • 所以字节码就是jvm能够理解的代码文件,即java.c。字节码不面向任何的特点定处理器,只面向虚拟机。java,编译与解释并存的语言。
  • 好处:实现了java的跨平台,一次编译多次运行。解决了传统的解释型语言执行效率低。

新生代和老年代的垃圾回收算法分别有哪些?区别是什么,

有没有遇到过频繁fullGC的问题,如何排查解决

关键字作用域

在这里插入图片描述

switch 语句能否作用在 byte 上, 能否作用在 long 上, 能否作用在 String 上?

在jdk 7 之前,switch 只能支持 byte、short、char、int 这几个基本数据类型和其对应的封装类型。switch后面的括号里面只能放int类型的值,但由于byte,short,char类型,它们会自动转换为int类型(精度小的向大的转化),所以它们也支持。
注意,对于精度比int大的类型,比如long、float,doulble,不会自动转换为int,如果想使用,就必须强转为int,如(int)float;
jdk1.7后,整形,枚举类型,boolean,字符串都可以。对于String来说,jdk1.7后并没有新的指令来处理switch string,而是通过调用switch中string.hashCode,将string转换为int从而进行判断。

final 关键字

  1. 修饰类
    final修饰类时,说明该类不能被继承
  2. 修饰方法
    当一个方法被final修饰后,就代表该方法无法被重写,但是可以被重载。但是!同时注意 如果父类的 final修饰的方法设置为private,则子类可以写一个同名的方法,此时 ,该同名的方法不再产生final的报错,而是在子类重新定义了一个方法(注:类的private方法会隐式地被指定为final方法。)
    在这里插入图片描述在这里插入图片描述
  3. 修饰变量
    这里就是比较重要的点了,final修饰的变量其实就相当于定义了一个常量,无法被修改的变量。在声明时就必须给变量赋值,不然在使用时构造器会报错。如果final修饰的是一个基本数据类型的变量,那么这个变量的值就定了,不能变了,而如果修饰的是一个引用变量,那么该变量存的是一个内存地址,该地址就不能变了,但是该内存地址所指向的那个对象还是可以变的,就像你记住了人家的单元号,但你不能管人家家里人员数量。
    在这里插入图片描述
    问题:
    为什么局部内部类和匿名内部类只能访问局部的final变量?
    外部类执行完后,内部类不一定被销毁(可能还没跑完),此时外部类中的局部变量已被回收。此时内部类就会引用一个不存在的变量,为了解决该问题,jvm就会将局部变量复制一份放入内部类中当作局部变量。那么此时,在复制局部变量时必须保证两个变量值时一样的(可能会在内部类中改变),那么此时就基于final关键字对变量进行初始化保证不变。

重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同。方法返回值和访问修饰符不是判别标准,且会报错。
重写:发生在父子类中,方法名、参数列表必须相同。返回值范围可以小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类,如果父类访问修饰符为private不可以去重写。

"=="和 equals 方法究竟有什么区别?

"= =“操作符专门用来比较两个变量的值是否相等, 也就是用于比较变量所对应的内存中所存储的数值是否相同, 要比较两个基本类型的数据或两个引用变量是否相等, 只能用”= ="操作符。如果一个变量指向的数据是对象类型的, 那么, 这时候涉及了两块内存, 对象本身占用一块内存(堆内存) , 变量也占用一块内存, 例如 Objet obj = new Object();变量 obj 是一个内存,new Object()是另一个内存, 此时, 变量 obj 所对应的内存中存储的数值就是对象占用的那块内存的首地址。 对于指向对象类型的变量, 如果要比较两个变量是否指向同一个对象, 即要看这两个变量所对应的内存中的数值是否相等, 这时候就需要用= =操作符进行比较。equals 方法是用于比较两个独立对象的内容是否相同, 就好比去比较两个人的长相是否相同, 它比较的两个对象是独立的。例如, 对于下面的代码:
String a=new String(“a”);
String b=new String(“a”);
两条 new 语句创建了两个对象, 然后用 a,b 这两个变量分别指向了其中一个对象, 这是两个不同的对象, 它们的首地址是不同的, 即 a 和 b 中存储的数值是不相同的, 所以, 表达式a= =b 将返回 false, 而这两个对象中的内容是相同的, 所以, 表达式 a.equals(b)将返回 true。
在实际开发中, 我们经常要比较传递进行来的字符串内容是否等, 例如, String input= …;input.equals(“quit” ), 许多人稍不注意就使用==进行比较了, 这是错误的, 随便从网上找几个项目实战的教学视频看看, 里面就有大量这样的错误。 记住, 字符串的比较基本上都是使用 equals 方法。
如果一个类没有自己定义 equals 方法, 那么它将继承 Object 类的 equals 方法, Object 类的equals 方法的实现代码如下:

boolean equals(Object o){
	return this==o;
}

这说明, 如果一个类没有自己定义 equals 方法, 它默认的 equals 方法(从 Object 类继承的)就是使用= =操作符, 也是在比较两个变量指向的对象是否是同一对象, 这时候使用 equals和使用==会得到同样的结果, 如果比较的是两个独立的对象则总返回 false。

静态变量和实例变量的区别?

在语法定义上的区别: 静态变量前要加 static 关键字, 而实例变量前则不加。在程序运行时的区别: 实例变量属于某个对象的属性, 必须创建了实例对象, 其中的实例变量才会被分配空间, 才能使用这个实例变量。 静态变量不属于某个实例对象, 而是属于类,所以也称为类变量, 只要程序加载了类的字节码, 不用创建任何实例对象, 静态变量就会被分配空间, 静态变量就可以被使用了。 总之, 实例变量必须创建对象后才可以通过这个对象来使用, 静态变量则可以直接使用类名来引用。
例如, 对于下面的程序, 无论创建多少个实例对象, 永远都只分配了一个 staticVar 变量, 并且每创建一个实例对象, 这个 staticVar 就会加 1; 但是, 每创建一个实例对象, 就会分配一个 instanceVar, 即可能分配多个 instanceVar, 并且每个 instanceVar 的值都只自加了 1 次。

public class VariantTest{
	public static int staticVar = 0;
	public int instanceVar = 0;
	public VariantTest(){
		staticVar++;
		instanceVar++;
		System.out.println(“staticVar=+ staticVar +,instanceVar=+ instanceVar);
	}
}

是否可以从一个 static 方法内部发出对非 static 方法的调用

不可以。 因为非 static 方法是要与对象关联在一起的, 必须创建一个对象后, 才可以在该对象上进行方法调用, 而 static 方法调用时不需要创建对象, 可以直接调用。 也就是说, 当一个 static 方法被调用时, 可能还没有创建任何实例对象, 如果从一个 static 方法中发出对非static 方法的调用, 那个非 static 方法是关联到哪个对象上的呢? 这个逻辑无法成立, 所以,一个 static 方法内部发出对非 static 方法的调用。

Integer 与 int 的区别

int 是 java 提供的 8 种原始数据类型之一。 Java 为每个原始类型提供了封装类, Integer 是 java为 int 提供的封装类。 int 的默认值为 0, 而 Integer 的默认值为 null, 即 Integer 可以区分出未赋值和值为 0 的区别, int 则无法表达出未赋值的情况。

ceil,floor,round取整

ceil 的英文意义是天花板, 该方法就表示向上取整
floor 的英文意义是地板, 该方法就表示向下取整
round 方法, 它表示“四舍五入” , 算法为 Math.floor(x+0.5), 即将原来的数字加上 0.5 后再向下取整

接口是否可继承接口? 抽象类是否可实现(implements)接口? 抽象类是否可继承具体类(concrete class)? 抽象类中是否可以有静态的 main 方法?

首先 接口的特点以及重点:
1、接口不能直接去实例化一个接口,因为接口中的方法都是抽象的,是没有方法体的。但是,我们可以使用接口类型的引用指向一个实现了该接口的对象(键盘、鼠标,…),并且可以调用这个接口中的方法。
2、一个类可以实现不止一个接口。键盘可以连接戴尔的接口、华硕的接口、联想的接口。
3、接口也可以继承,并且可以多继承。一个接口可以继承于另一个接口,或者另一些接口。比如现在的很多人电脑的usb接口不够用,所以有了usb接口拓展。
4、接口中所有的方法都是抽象的和public的,所有的属性都是public,static,final的。
5、一个类如果要实现某个接口的话,那么它必须要实现这个接口中的所有方法。
6、接口中不能有main方法,main方式是静态方法,静态方式是需要分配内存空间,而接口是表现形式,没有分配空间。
其次 看一下抽象类的概念:
抽象就是找出一些事物的相似和共性之处, 然后将这些事物归为一个类, 这个类只考虑这些事物的相似和共性之处, 并且会忽略与当前主题和目标无关的那些方面, 将注意力集中在与当前目标有关的方面。
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。具有抽象方法的类一定为抽象类。在Java中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。
那么接着回答问题:
1、接口是可以被接口继承的。即通过关键字extends声明一个接口是另一个接口的子接口。由于接口中的方法和常量都是public,子接口将继承父接口中的全部方法和常量。
2、抽象类可以实现接口,当一个类声明实现一个接口而没有实现接口中所有的方法,那么这个必须是抽象类,即abstract类。
3、抽象类是可以继承实体类。
4、抽象类中可以有静态的main方法。

面向对象的特征有哪些方面

万物皆对象,相对于面向过程(直接高效),面向对象更注重需求中有哪些参与者(更易于扩展、复用、维护)。

  1. 封装:封装的意义在于明确外部可以使用的成员函数和变量。(内部逻辑必须由javabean本身决定,不能由外部胡乱修改)
    封装是保证软件部件具有优良的模块性的基础, 封装的目标就是要实现软件部件的“高内聚、低耦合” , 防止程序相互依赖性而带来的变动影响。 在面向对象的编程语言中, 对象是封装的最基本单位, 面向对象的封装比传统语言的封装更为清晰、 更为有力。 面向对象的封装就是把描述一个对象的属性和行为的代码封装在一个“模块” 中, 也就是一个类中, 属性用变量定义, 行为用方法进行定义, 方法可以直接访问同一个对象中的属性。
    封装步骤 1)私有化成员变量,使用private关键字修饰。 2) 提供公有的get和set方法,并在方法体中进行合理值的判断。 3) 在构造方法中调用set方法进行合理值的判断。
  2. 继承:继承基类的方法,做出自己的改变和扩展。
    当多个类之间有相同的特征和行为时,可以将相同的内容提取出来组成一个公共类,让多个类吸收公共类中已有特征和行为而在多个类型只需要编写自己独有特征和行为的机制,叫做继承。
    1、子类不能继承父类的构造方法和私有方法,但私有成员变量可以被继承,只是不能直接访问。
    2、无论使用何种方式构造子类的对象时都会自动调用父类的无参构造方法,来初始化从父类中继承的成员变量,相当于在构造方法的第一行增加代码super()的效果。
  3. 多态:继承,方法重写,父类引用指向子类对象。
    多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定, 而是在程序运行期间才确定, 即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法, 必须在由程序运行期间才能决定。
    其实际意义在于屏蔽不同子类的差异性实现通用的编程带来不同的效果。
    多态弊端:无法调用子类特有的功能。

java 中实现多态的机制是什么?

靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象, 而程序调用的方法在运行期才动态绑定, 就是引用变量所指向的具体实例对象的方法, 也就是内存里正在运行的那个对象的方法, 而不是引用变量的类型中定义的方法。

抽象类和 接口 有什么区别?

  • 抽象类中可以存在普通成员方法,而接口中只能存在public static方法。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的
  • 抽象类只能继承一个,接口可以实现多个。

接口的设计目的,是对类的行为进行约束(更准确的说是一种有的约束,因为接口不能规定类不能有什么行为),至于如何实现不去限制。
抽象类的设计目的是为了代码复用,当不同的类具有相同的行为时,可以将共同的行为派生出一个抽象类。可以说是现有子类,再有父类。抽象类中可以有抽象方法,也可以有实现的方法。抽象类是不允许实例化的,因为有些方法是没有实现的。抽象类的本质是 is a的关系。如:bmw is a car。而接口的本质是like a关系。如bird like a aircraft。

list和set的区别

  • List:有序,按对象进入顺序进行保存,可重复,允许多个null对象。可以使用iterator取出所有元素,还可以使用 get(int index)来随机方法。
  • Set:无序,不可重复,最多允许有一个null对象,取元素时只能使用iterator来逐一遍历各个元素。

ArrayList和LinkedList区别

  • ArrayList:基于动态数组,是连续内存存储,适合下标(随机访问),扩容机制:因为数组长度固定,超出长度后需要重建数组,然后将旧数组拷贝到新数组,再回收掉旧数组。如果使用尾插法并指定初始容量可以极大提高性能,甚至超过linkedlist。
  • LinkedList:基于链表,是分散存储在内存中的,适合做数据的插入和删除操作,不适合查询:需要逐一遍历。插入元素时会创建node对象。遍历linkedlist时必须使用迭代器,而建议使用for循环(效率低,需要一个一个node去遍历才能找到下标),也不建议使用indexof返回元素索引(也会全局遍历)。

内部类可以引用它的包含类的成员吗? 有没有什么限制?

完全可以。 如果不是静态内部类, 那没有什么限制!
如果你把静态嵌套类当作内部类的一种特例, 那在这种情况下不可以访问外部类的普通成员变量, 而只能访问外部类中的静态成员。

Anonymous Inner Class (匿名内部类) 是否可以 extends(继承)其它类, 是否可以 implements(实现)interface(接口)?

可以继承其他类或实现其他接口。 不仅是可以, 而是必须!

String/StringBuffer/StringBuilder区别与使用

String:不可变字符序列
StringBuffer:可变字符序列、效率低、线程安全(底层使用synchronized关键字修饰)
StringBuilder(jdk1.5):可变字符序列、效率高、线程不安全

// String使用陷阱:
String s = "a";
s = s + "b";
// 因为实际上“a”字符串对象已经丢弃,现在又产生了另一个字符串str+“b”,
// 如果多次执行这些改变字符串内容的操作,会导致大量的副本存留在内存中,降低效率。

三者之间的区别:
(1)字符修改上的区别(主要,见上面分析)
(2)初始化上的区别,String可以空赋值,后者不行,报错
StringBuffer s = null; //结果警告:Null pointer access: The variable result can only be null at this location
StringBuffer s = new StringBuffer();//StringBuffer对象是一个空的对象
另外, String 实现了 equals 方法, new String(“abc” ).equals(new String(“abc” )的结果为 true,而 StringBuffer 没有实现 equals 方法,所以, new StringBuffer(“abc” ).equals(new StringBuffer(“abc” )的结果为 false。
默认长度:
首先明确 StringBuffer类与 StringBuilder类均继承了抽象类 AbstractStringBuilder类。
源码中StringBuffer类和StringBuilder类初始化均调用父类的构造方法:子类默认传入值16 给父类初始化字符数组
扩容算法:
使用append()方法在字符串后面追加东西的时候,如果长度超过了该字符串存储空间大小了就需要进行扩容:构建新的存储空间更大的字符串,将旧数据复制过去;
尝试将新容量扩为 大小:变成2倍+2,容量如果还不够,直接扩充到需要的容量大小;
小结:
(1)如果要操作少量的数据用 String;
(2)多线程操作字符串缓冲区下操作大量数据 StringBuffer;
(3)单线程操作字符串缓冲区下操作大量数据 StringBuilder。

String s = new String(“xyz”);创建了几个 String Object? 二者之间有什么区别?

两个或一个, ” xyz” 对应一个对象, 这个对象放在字符串常量缓冲区, 常量” xyz” 不管出现多少遍, 都是缓冲区中的那一个。 New String 每写一遍, 就创建一个新的对象, 它一句那个常量” xyz” 对象的内容来创建出一个新 String 对象。 如果以前就用过’ xyz’ , 这句代表就不会创建” xyz” 自己了, 直接从缓冲区拿。

如何把一段逗号分割的字符串转换成一个数组?

//  方法一:String api中的 split方法
        String orgStr="a,b,c,d,e,f";
        String [] result = orgStr.split(",");

        for(int a = 0;a<result.length;a++){
            System.out.print(result[a]+"\t");
        }
// 方法二: StingTokenizer
		String orgStr="ab,b,c,d,e,f";  
        StringTokenizer  tokener = new StringTokenizer(orgStr,",");
        String [] result = new String[tokener .countTokens()];
          
        int i=0;
        while(tokener. hasMoreTokens ())
             {
               result[i++]=tokener.nextToken();
             }
        for(int a = 0;a<result.length;a++){
           System.out.print(result[a]+"\t");
        }

为什么数组有length属性,而字符串没有?或者,为什么字符串有length()方法,而数组没有?

数组是一个容器对象,其中包含固定数量的同一类型的值。一旦数组被创建,他的长度就是固定的了。数组的长度可以作为final实例变量的长度。因此,长度可以被视为一个数组的属性。
这里可能会有一个疑问,既然数组大小是初始化时就规定好的,那么int[][] arr = new int[3][];定义的数组并没有给出数组的第二维的大小,那么这个arr的长度到底是如何“规定好”的呢?
其实,arr的长度就是3。其实Java中所有的数组,无论几维,其实都是一维数组。例如arr,分配了3个空间,每个空间存放一个一维数组的地址,这样就成了“二维”数组。但是对于arr来说,他的长度就是3。

int[][] arr1 = new int[3][];
System.out.println(arr1.length); //3
int[][] arr2 = new int[3][5];
System.out.println(arr2.length); //3

那为什么String有length()方法?
String背后的数据结构是一个char/byte数组,所以没有必要来定义一个不必要的属性(因为该属性在char/byte数组中已经提供了)。

下面这条语句一共创建了多少个对象: Strings=“a”+“b”+“c”+“d”;

题目中的第一行代码被编译器在编译时优化后, 相当于直接定义了一个” abcd” 的字符串。所以, 上面的代码应该只创建了一个 String 对象。

try {}里有一个 return 语句, 那么紧跟在这个 try 后的 finally {}里的 code 会不会被执行, 什么时候被执行, 在 return 前还是后?

看两个例子

int x= 1;
        try{
            return x;
        }
        finally
        {
            x++; //or ++x 结果一样
        }
// 最终返回1

int x= 1;
        try{
            return x;
        }
        finally
        {
            return 2;
        }
// 最终返回2

会执行,在方法返回调用者前执行。Java允许在finally中改变返回值的做法是不好的,因为如果存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值,然后如果在finally中修改了返回值,这会对程序造成很大的困扰。
也许你的答案是在return之前,但往更细地说,我的答案是在return中间执行。

error 和 exception 有什么区别?

error 表示恢复不是不可能但很困难的情况下的一种严重问题。 比如说内存溢出。 不可能指望程序能处理这样的情况。 exception 表示一种设计或实现问题。 也就是说, 它表示如果程序运行正常, 从不会发生的情况。

Java 中的异常处理机制的简单原理和应用。

异常是指 java 程序运行时(非编译) 所发生的非正常情况或错误。
Java 对异常进行了分类, 不同类型的异常分别用不同的 Java 类表示, 所有异常的根类为java.lang.Throwable, Throwable 下面又派生了两个子类: Error 和 Exception。
Error 表示应用程序本身无法克服和恢复的一种严重问题, 程序只有死的份了, 例如, 说内存溢出和线程死锁等系统问题。
Exception 表示程序还能够克服和恢复的问题, 其中又分为系统异常和普通异常, 系统异常是软件本身缺陷所导致的问题, 也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题, 但在这种问题下还可以让软件系统继续运行或者让软件 死 掉 , 例 如 , 数 组 脚 本 越 界 ( ArrayIndexOutOfBoundsException ) , 空 指 针 异 常( NullPointerException) 、 类转换异常( ClassCastException) ; 普通异常是运行环境的变化或异常所导致的问题, 是用户能够克服的问题, 例如, 网络断线, 硬盘空间不够, 发生这样的异常后, 程序不应该死掉。
java 为系统异常和普通异常提供了不同的解决方案, 编译器强制普通异常必须 try…catch 处理或用 throws 声明继续抛给上层调用方法处理, 所以普通异常也称为 checked 异常, 而系统异常可以处理也可以不处理, 所以, 编译器不强制用 try…catch 处理或用 throws 声明, 所以系统异常也称为 unchecked 异常。

请写出你最常见到的 5 个 runtimeexception。

这道题主要考你的代码量到底多大, 如果你长期写代码的, 应该经常都看到过一些系统方面的异常, 你不一定真要回答出 5 个具体的系统异常, 但你要能够说出什么是系统异常, 以及几个系统异常就可以了, 当然, 这些异常完全用其英文名称来写是最好的, 如果实在写不出,那就用中文吧, 有总比没有强!
所谓系统异常, 就是……, 它们都是 RuntimeException的子类, 在 jdk doc中查 RuntimeException类, 就可以看到其所有的子类列表, 也就是看到了所有的系统异常。 我比较有印象的系统异常有:
ArithmeticException(算术运算异常)
NullPointerException(空指针异常)
ArrayIndexOutOfBoundsException(数组越界异常)
NegativeArraySizeException(数组长度为负数)
IOException(文件未找到、未打开或者I/O操作不能进行而引起异常)
ClassCastException(类型转换异常)
在这里插入图片描述

JAVA 语言如何进行异常处理, 关键字: throws,throw,try,catch,finally 分别代表什么意义? 在 try 块中可以抛出异常吗?

throws是获取异常
throw是抛出异常
try是将会发生异常的语句括起来,从而进行异常的处理,
catch是如果有异常就会执行他里面的语句,
而finally不论是否有异常都会进行执行的语句。
throw和throws的详细区别如下:
throw是语句抛出一个异常。
语法:throw (异常对象);
throw e;
throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常)
语法:(修饰符)(方法名)([参数列表])[throws(异常类)]{…}
public void doA(int a) throws Exception1,Exception3{…}

java 中有几种方法可以实现一个线程? 用什么关键字修饰同步方法? stop()和 suspend()方法为何不推荐使用?

java5 以前,有两种实现方法,:
自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法。
自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法。
第一种:
new Thread(){}.start();这表示调用 Thread 子类对象的 run 方法, newThread(){}表示一个 Thread的匿名子类的实例对象, 子类加上 run 方法后的代码如下:

new Thread(){
	public void run(){
	}
}.start();

第二种:
new Thread(newRunnable(){}).start();这表示调用 Thread 对象接受的 Runnable 对象的 run 方法, newRunnable(){}表示一个 Runnable 的匿名子类的实例对象,runnable 的子类加上 run 方法后的代码如下:

new Thread(newRunnable(){
		public void run(){
		}
	}
).start();

从 java5 开始, 又出了两种:
第三种:自定义类实现Callable接口并重写call方法,创建该类的对象作为实参来构造FutureTask类型的对象(用于接收线程运算结果),然后使用FutureTask来构造Thread类型的对象去调用start方法。
第四种:线程池来实现,线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提交了响应速度。步骤:创建线程池,执行任务void execute(通常用于执行Runnable)/ Future submit(通常用于执行Callable),最后关闭线程池shutdown() 。

ExecutorService pool= Executors.newFixedThreadPool(3)
for(inti=0;i<10;i++){
	pool.execute(new Runable(){public voidrun(){}});
}
Executors.newCachedThreadPool().execute(newRunable(){public void run(){}});
Executors.newSingleThreadExecutor().execute(newRunable(){public void run(){}});

Callable接口和Runnable接口的不同之处:
1.Callable规定的方法是call,而Runnable是run
2.call方法可以抛出异常,但是run方法不行,因此在run方法执行中发生的任何检查异常,必须在线程中处理。
3.Callable对象执行后可以有返回值,运行Callable任务可以得到一个Future对象,通过Future对象可以了解任务执行情况,可以取消任务的执行,而Runnable不可有返回值
用什么关键字修饰同步方法?
用synchronized 关键字修饰同步方法。
stop()和 suspend()方法为何不推荐使用?
反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们,结果很难检查出真正的问题所在。
suspend() 方法容易发生死锁。调用 suspend() 的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被" 挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用 suspend() ,而应在自己的 Thread 类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait() 命其进入等待状态。若标志指出线程应当恢复,则用一个 notify()重新启动线程。
为什么不推荐使用线程池?
executors.newCachedThreadPool和newScheduledThreadPool两个方法的最大线程数Integer.Max_value,如果达到上限就没有任何服务器可以继续工作,肯定会抛出oom异常。然后newSingleThreadExecutor和newFixedThreadPool两个方法的workqueue参数为newLinkedBlockingQueue(),容量也是Integer.Max_value(2 147 483 647)。
在这里插入图片描述

B/S,C/S区别

b/s的优点:

  • 分布性强,客户端零维护。只要有网络、浏览器,可以随时随地进行查询、浏览等业务处理。
  • 业务扩展简单方便,通过增加网页即可增加服务器功能。
  • 维护简单方便,只需要改变网页,即可实现所有用户的同步更新。
  • 开发简单,共享性强。

b/s的缺点:

  • 个性化特点明显降低,无法实现具有个性化的功能要求。不过随着html5的普及,这个缺点越来越弱化了。
  • 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度明显降低(Ajax可以一定程度上解决这个问题)。

c/s的优点:

  • 能充分发挥客户端PC的处理能力,很多工作可以在客户端处理后再提交给服务器,所以CS客户端响应速度快。
  • 操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求。
  • 安全性能可以很容易保证,C/S一般面向相对固定的用户群,程序更加注重流程,它可以对权限进行多层次校验,提供了更安全的存取模式,对信息安全的控制能力很强。一般高度机密的信息系统采用C/S结构适宜。

** c/s的缺点:**

  • 需要专门的客户端安装程序,分布功能弱,针对点多面广且不具备网络条件的用户群体,不能够实现快速部署安装和配置。
  • 兼容性差,对于不同的开发工具,具有较大的局限性。若采用不同工具,需要重新改写程序。
  • 开发、维护成本较高,需要具有一定专业水准的技术人员才能完成,发生一次升级,则所有客户端的程序都需要改变。。
  • 用户群固定。由于程序需要安装才可使用,因此不适合面向一些不可知的用户,所以适用面窄,通常用于局域网中。

sleep() 和 wait() 有什么区别?

sleep 是线程类( Thread) 的方法, 导致此线程暂停执行指定时间, 给执行机会给其他线程, 但是监控状态依然保持, 到时后会自动恢复。如果当前线程进入了同步锁, sleep 方法并不会释放锁, 即使当前线程使用 sleep 方法让出了 cpu, 但其他被同步锁挡住了的线程也无法得到执行。
wait 是 Object 类的方法, 对此对象调用 wait 方法导致本线程放弃对象锁, 进入等待此对象的等待锁定池, 只有针对此对象发出 notify 方法( 或 notifyAll) 后本线程才进入对象锁定池准备获得对象锁进入运行状态。

同步和异步有何异同, 在什么情况下分别使用他们? 举例说明。

如果数据将在线程间共享。 例如正在写的数据以后可能被另一个线程读到, 或者正在读的数据可能已经被另一个线程写过了, 那么这些数据就是共享数据, 必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法, 并且不希望让程序等待方法的返回时, 就应该使用异步编程, 在很多情况下采用异步途径往往更有效率。

多线程有几种实现方法?同步有几种实现方法?

多线程有两种实现方法, 分别是继承 Thread 类与实现 Runnable 接口
同步的实现方面有两种, 分别是 synchronized,wait 与 notify
wait():使一个线程处于等待状态, 并且释放所持有的对象的 lock。
sleep():使一个正在运行的线程处于睡眠状态, 是一个静态方法, 调用此方法要捕捉InterruptedException 异常。
notify():唤醒一个处于等待状态的线程, 注意的是在调用此方法的时候, 并不能确切的唤醒某一个等待状态的线程, 而是由 JVM 确定唤醒哪个线程, 而且不是按优先级。
Allnotity():唤醒所有处入等待状态的线程, 注意并不是给所有唤醒线程一个对象的锁, 而是让它们竞争。

启动一个线程是用 run()还是 start()? .

启动一个线程是调用 start()方法, 使线程就绪状态, 以后可以被调度为运行状态, 一个线程必须关联一些具体的执行代码, run()方法是该线程所关联的执行代码。

当一个线程进入一个对象的一个 synchronized 方法后, 其它线程是否可进入此对象的其它方法?

分几种情况:
1.其他方法前是否加了 synchronized 关键字, 如果没加, 则能。
2.如果这个方法内部调用了 wait, 则可以进入其他 synchronized 方法。
3.如果其他个方法都加了 synchronized 关键字, 并且内部没有调用 wait, 则不能。
4.如果其他方法是 static, 它用的同步锁是当前类的字节码, 与非静态的方法不能同步,因为非静态的方法用的是 this。

简述 synchronized 和 java.util.concurrent.locks.Lock 的异同?

主要相同点: Lock 能完成 synchronized 所实现的所有功能
主要不同点: Lock 有比 synchronized 更精确的线程语义和更好的性能。 synchronized 会自动释放锁, 而 Lock 一定要求程序员手工释放, 并且必须在 finally 从句中释放。 Lock 还有更强大的功能, 例如, 它的 tryLock 方法可以非阻塞方式去拿锁。

子线程循环 10 次, 接着主线程循环 100, 接着又回到子线程循环 10 次,接着再回到主线程又循环 100, 如此循环 50 次, 请写出程序。

package com.lagou.kafka.demon.config;

public class ThreadTest {
    /**
     * @param args
     */
    public static void main(String[] args) {
        new ThreadTest().init();
    }

    public void init() {
        final Business business = new Business();
        new Thread(
                new Runnable() {
                    public void run() {
                        for(int i=0;i<50;i++) {
                            business.SubThread(i);
                        }
                    }
                }
        ).start();
        for(int i=0;i<50;i++)
        {
            business.MainThread(i);
        }
    }

    private class Business
    {
        boolean flag = true;//这里相当于定义了控制该谁执行的一个信号灯
        public synchronized void MainThread(int i) {
            if(flag)
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            for(int j=0;j<100;j++) {
                System.out.println(Thread.currentThread().getName()+ ":i=" + i +",j=" + j);
            }
            flag= true;
            this.notify();
        }

        public synchronized void SubThread(int i) {
        if(!flag)
            try {
                this.wait();
            } catch (InterruptedException e)
            { e.printStackTrace();
                            }


        for(int j=0;j<10;j++) {
            System.out.println(Thread.currentThread().getName()+ ":i=" + i +",j=" + j);
        }
        flag= false;
        this.notify();
        }
    }
}

介绍 Collection 框架的结构

java.util.Collection接口是List接口、Queue 接口以及Set接口的父接口,因此该接口里定义的方法既可用于操作List集合,也可用于操作Queue集合和Set集合。

  • List集合
    该集合中允许有重复的元素并且有先后放入次序。该集合的主要实现类有:ArrayList类、LinkedList类、Stack类、Vector类。
    其中ArrayList类的底层是采用动态数组进行数据管理的,支持下标访问,增删元素不方便。
    其中LinkedList类的底层是采用双向链表进行数据管理的,访问不方便,增删元素方便。可以认为ArrayList和LinkedList的方法在逻辑上完全一样,只是在性能上有一定的差别,ArrayList更适合于机访问而LinkedList更适合于插入和删除;在性能要求不是特别苛刻的情形下可以忽略这个差别。
  • set集合

hashmap

hashMap术语介绍:

  • 桶: 就是hashmap的table数组
  • bin: 就是挂在数组上的链表
  • TreeNode: 红黑树
  • Capacity: table总容量
  • Min_TREEIFY_Capacity: 64 转化为红黑树table最小大小
  • TREEIFY_THRESHOLD: 8 转化为红黑树的阈值
  • loadFactor: 0.75 table扩容因子,当实际length大于等于 capacity*loadFactor时会进行扩容,并且扩容是按照2的整数次幂。
  • threshold: capacity*loadFactor 扩容阈值

在JDK1.7以及1.7版本之前,其底层原理是基于数组+链表来实现的,这样链表一旦长了,效率会变得比较低。
在1.8中对HashMap的数据结构进行了一定的优化,其中增加了一个阈值对数组元素进行判断是否有必要进行红黑树变形(红黑树是一种二叉查找树),一旦链表长度达到了阈值,其数据结构便会变形为红黑树,提高了查询效率,但插入的效率并没有链表头插法那么高,每次插入新的数据,都得维护红黑树的结构。这样算是对查找和插入元素时性能的一个权衡。
在这里插入图片描述
存储过程:

  • key的hash值二次哈希后对数组长度取模,对应到数组下标。
  • 如果没有产生hash冲突,数组下标位置没有元素,则直接创建node存入数组
  • 如果产生hash冲突,现进行equals比较,相同则取代该元素,不同则判断链表高度插入链表。链表高度为8,并且数组长度达到64时,链表转化为红黑树。长度低于6时,红黑树又会退化成链表。
  • key=null,直接放在下标为0的位置。
// 通过hashmap的get/set方法可知,hashmap是使用key/value来对数据进行存储/获取的。
import java.util.HashMap;
public class HashmapTest {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        map.put("一","关羽");
        map.put("二","赵云");
        map.put("三","张飞");
        System.out.println(map.get("二"));
    }
}

// 链表的实现
public class Node {
    private Object content;
    private Node next;

    public Node(Object content, Node next) {
        this.content = content;
        this.next = next;
    }

    public static void main(String[] args) {
        // 创建头节点
        Node header = new Node(new Object(), null);
        // 向头节点后加入节点
        header.next = new Node(new Object(), null);
        
        // 重新插入一个头节点
        // Node header = new Node(new Object(), header); 
    }
}
// 向链表中头节点put实现jdk1.7头算法
	put(key,value){
		int hashcode = key.hashcode();
		int index = hashcode % table.length; //模拟算法 实际底层是按位与来实现的	
		
		table[index] = new Entry(key, value, table[insex]);
	}

为什么用数组?

1)数组效率高
在HashMap中,定位元素的位置利用元素的key的哈希值对数组长度取模得到。此时,我们已得到桶的位置。显然数组的查找效率比LinkedList大。
2)可自定义扩容机制
采用基本数组结构,扩容机制可以自己定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率高。

为什么用链表?

因为要处理hash冲突,即Key不同但是生成的hashcode一样的情况,只有key,hashcode都一样才可以覆盖,不然是要保存起来的,这时候就需要使用链表来存储。
向链表的头部插入数据速度是快于向尾部插入的,原因是:
向头部插入时,只需要指定当前插入值的指向为前头部即可完成。而向尾部插入时,要去找尾节点,此时便需要遍历整个链表。(注:插入到头部后,要使链表向下移动一个单元,保证链表的头部是位于数组的节点上。)

为什么初始容量是2的整数次幂?为什么要按位与?

在hashmap底层是基于按位与来实现数组中index的计算的,那么如下:

// 假设将随机数 0101 0101 计算table中的index
table.legth = 16
	 			  	    0101 0101
table.legth - 1  = 15 : 0000 1111
此时高四位都是0,低四位都是1,按位与后=0000 0101

假设table.legth = 17
	 				    0101 0101
table.legth - 1  = 16 : 0001 0000
此时低四位全是0,那么按位与后 = 0001 0000,
这时就会导致 0000 00000001 0000两个index被大量使用,而其他的index没有数据插入
故,容量需要2的整数次幂。

接着,为什么要按位与计算?是与jdk有关,jdk1.4时,取余计算速度很慢,而按位与是基于bit位来计算,hashmap作者就使用了按位与。

HashMap 和 Hashtable 的区别

HashMap 是 Hashtable 的轻量级实现(非线程安全的实现),hashtable每一个方法都加了synchronized。他们都完成了 Map 接口, 主要区别在于 HashMap 允许空(null) 键值(key) ,由于非线程安全, 在只有一个线程访问的情况下, 效率要高于 Hashtable。
HashMap 允许将 null 作为一个 entry 的 key 或者 value, 而 Hashtable 不允许。
HashMap把 Hashtable 的contains方法去掉了, 改成 containsvalue和 containsKey。因为 contains方法容易让人引起误解。
最大的不同是, Hashtable 的方法是 Synchronize 的, 而 HashMap 不是, 在多个线程访问Hashtable 时, 不需要自己为它的方法实现同步, 而 HashMap 就必须为之提供外同步。
Hashtable 和 HashMap 采用的 hash/rehash 算法都大概一样, 所以性能不会有很大的差异。

ConcurrentHashMap原理

  • jdk7:ConcurrentHashMap的数据结构是 Reentrantlock+Segment+HashEntry,一个segment中包含一个hashentry数组,每个hashentry又是一个链表结构。
    锁: segment分段锁,锁定操作的segment,其他segment不受影响。并发度为segment的个数,数组扩容不会影响其他的segment。
    元素查询: 会进行二次hash,第一次hash会定位到segment,第二次会定位到链表的头部。
    get方法无需加锁,内部变量由volatile保证。
  • jdk8:synchronyzed+CAS+node+红黑树。

常见的哈希算法与碰撞算法

哈希算法

  • 直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
  • 数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。
  • 除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
  • 分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
  • 平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。 伪随机数法:采用一个伪随机数当作哈希函数

解决碰撞算法

  • 开放定址法
    开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
  • 链地址法
    将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
  • 再哈希法
    当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
  • 建立公共溢出区
    将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值