首页 > 技术文章 > Java 基础

huang580256 2021-07-01 15:19 原文

1.深Clone()和浅Clone()

在实际编程中,经常会遇到从某个已有的对象A创建出另一个与A具有相同状态的对象B,并且对B的修改不会影响到A,例如在Prototype(原型模式)中,就需要clone一个对象的实例,当类中只有一些基本数据类型时,浅clone,返回super.clone()即可,当类中有非基本数据类型时,需要用到深clone()了,实现方法是在对象调用clone()方法完成复制后,接着对对象中的非基本类型的属性也调用clone()方法完成深复制

实现Clone()的步骤:

  1. 实现Clone()的类首先要实现Cloneable接口,Cloneable()接口是一个标识接口,没有任何的接口方法
  2. 在类中重写Object类的clone方法
  3. 在clone()方法中调用super.clone()方法
  4. 把浅复制的引用指向原型对象新的克隆体
  • 浅Clone()
public class Obj implements Cloneable{
    private int aInt=0;

    public int getaInt() {
        return aInt;
    }

    public void setaInt(int int1) {
        aInt = int1;
    }

    public void changeInt(){
        this.aInt=1;
    }

    public Obj clone() throws CloneNotSupportedException {
	obj o=null;
        o = (Obj) super.clone();
        return o;
    }
}
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Obj a=null;
        Obj b=(Obj) a.clone();
        b.changeInt();
        System.out.println("a: "+a.getaInt());
        System.out.println("b: "+b.getaInt());
    }
}

输出:
image

  • 深clone()
import java.util.Date;

public class Obj implements Cloneable{
    private Date birth=new Date();

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    public void changeDate(){
        this.birth.setMonth(1);
    }

    public Obj clone() throws CloneNotSupportedException {
        Obj o=null;
        o = (Obj) super.clone();
        //实现深复制clone()
        o.birth =(Date) this.getBirth().clone();
        return o;
    }
}
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Obj a=new Obj();
        Obj b=(Obj) a.clone();
        b.changeDate();
        System.out.println(a.getBirth());
        System.out.println(b.getBirth());
    }
}

输出:
image

浅复制和深复制有何区别

浅复制:被复制对象的所有变量都含有与原来对象相同的值,而所有对其他对象的引用仍指向原来的对象,换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。

深复制:被复制对象的所有变量都含有与原来对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制的新的对象,而不再是原来那些被引用的对象,换而言之,深复制把复制的对象所引用的对象都复制了一遍。

eg:假如定义一个类

class Test{
        public int i;
        public StringBuffer s;
    }

image

常见题目:创建对象的几种方式

有4种显式地创建对象的方式:

1.用new语句创建对象,这是最常用的创建对象的方式。

2.运用反射手段,调用Java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。

3.调用对象的clone()方法。

4.运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法.

2.反射

  • 反射机制是java语言非常重要的一个特性,它允许程序在运行时进行自我检查,同时也允许对内部成员进行操作

反射机制提供的主要功能有:得到一个对象所属的类;获取一个类的所有成员变量和方法;在运行时创建对象;在运行时调用对象的方法

eg:在运行时动态的创建类的对象:

package com.basic.reflect;

public class Sub extends Base {
    public void f(){
        System.out.println("Sub");
    }
}

package com.basic.reflect;

public class Base {
    public void f(){
        System.out.println("Base");
    }
}

package com.basic.reflect;

public class Test {
    public static void main(String[] args)  {
        try {
            Class c=Class.forName("com.basic.reflect.Sub");
            Base b=(Base) c.newInstance();
            b.f();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

Q:反射机制中,class是一个非常重要的类,如何才能获取到class类呢?
1.Class.forName("类的路径")

2.类名.class

3.实例.getClass();

3.package有什么作用

1.提供多层命名空间,解决命名冲突,通过使用package,使得处于不同的package中的类可以存在相同的名字

2.对类按功能进行分类,使项目的组织更加清晰

4.继承的特性

1.Java不支持多重继承,子类最多只能有一个父类,但是可以实现多个接口来达到多重继承的目的

2.子类只能继承父类的非私有(public,protected)成员变量和方法

3.子类定义的成员变量与父类中定义的成员变量相同时,子类中的成员变量会覆盖父类的成员变量,而不会继承

4.子类中的方法与父类中的方法有相同的函数签名(相同方法名,参数个数与类型时),子类会覆盖父类的方法,而不会继承

5.多态的实现机制

  • 多态是面向对象的一个非常重要的机制,它表示同一个操作作用在不同的对象时,会有不同的语意,从而产生不同的结果,如同样执行"+"操作,"1+2"用来实现正数相加,而"1"+"2"实现了字符串的拼接,多态主要有以下两种变现方式:
    1.方法的重载:一个类中有多个同名的方法,但这些方法有着不同的参数,因此在编译时可以确定到底调用哪个方法(编译时多态)

2.方法的覆盖:子类可以覆盖父类的方法,因此同样的方法在父类和子类中有着不同的表现形式(运行时多态)

6.抽象类(abstract class)和接口(Interface)有何异同

如果一个类中有抽象方法,那么这个类就是抽象类,可以通过把类或者类中的某些方法声明为abstract(abstract只能用来修饰类或方法,不能用来修饰属性)来表示一个类是抽象类,接口是一个方法的集合,接口中的方法没有方法体

只要包含一个抽象方法的类就必须被声明为抽象类,抽象类可以声明方法的存在而不去实现它,被声明的方法不能包含方法体,在实现时,必须包含相同的或者更低级别的访问级别(public>protected>private)。抽象类在在使用的过程中不能被实例化,但是可以创建一个对象使其指向具体子类的一个实例。抽象类的子类为父类中的所有抽象方法提供具体的实现,否则他们也是抽象类

接口与抽象类的相同点如下:
1.都不能被实例化

2.接口的实现类或抽象类的子类都只有在实现了接口或抽象类中的方法后才能被实例化

接口与抽象类的不同点如下:
1.接口只有定义,其方法不能在接口中实现,只有实现接口的类才能实现接口中的方法,抽象类可以有定义和实现,其方法可以在抽象类中被实现

2.接口只能被实现(implement),抽象类只能被继承(extend),一个类可以实现多个接口,但一个类只能继承一个抽象类,因此使用接口可以间接达到多重继承的目的

3.接口强调特定功能的实现,是“has - a”的关系,而抽象类强调所属关系,是“is - a”的关系

4.接口中的成员变量默认为public static final,只能有静态的不能被修饰的成员,必须赋初值,且所有成员方法只能被public 和abstract关键字修饰,抽象类可以有自己的数据成员变量,也可以有非抽象的成员方法

7.获取父类的类名

不能通过super.getClass().getName()获取父类的类名,在java中任何类都继承自Object类,this.getClass()和super.getClass()都调用的object中的getClass()方法,而Object中getClass()方法的释意是:返回此object的运行时类,即实际运行类,

如何才能在子类中得到父类的名字呢?可以通过java的反射机制,使用getClass().getSuperclass().getName()

8.final,finally,finalize有什么区别

1.fianl用来声明属性,方法和类,分别表示属性不可变(引用不可变,非对象不可变),方法不可覆盖(不允许任何子类重写这个方法,但子类任可以使用这个方法),类不可被继承(不能派生出新的子类)

2.finally作为异常处理的一部分,只能用在try/catch中,并且附带一个语句块,表示这段语句最终一定会被执行

3.fianlize是Object类中的一个方法,在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其他资源的回收

Q:JDK中的哪些类是不能被继承的?
不能被继承的了类是哪些用final关键字修饰的类,一般比较基本的类型或防止扩展类无意间破坏原来方法实现的类型都应该是final的,在JDK中,String,StringBuffer都是基本类型,不被继承。

9.assert(断言)有什么作用

assert作为一种软件测试方法,提供了一种在代码中进行正确性检查的机制,。它的只要作用是对一个boolean表达式进行检查,一个正确运行的程序必须保证这个表达式为true,若boolean表达式的值为false,则表示程序已经处在一种不正确的状态下,系统需要提供警告信息并且退出程序,在还实际开发,assert主要用来保证呈程序的正确性。

assert包括两种表达式,分别为assert expression1 与 assert expression1 : experssion2,其中expression1表示一个boolean表达式,expression2表示一个基本类型或对象,基本类型包括boolean,char,double,float,int和long

10.volatile作用

在用Java语言编写的程序中,有时为了提高程序的运行效率,编译器会自动对其进行优化,把经常被访问的变量缓存起来,程序在读取这个变量时有可能会直接从缓存(例如寄存器)中来读取这个值,而不会去内存中读取。这样做的一个好处是提高了程序的运行效率,但当遇到多线程编程时,变量的值可能因为别的线程而改变了,而该缓存的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致,例如,在本次线程内,当读取一个变量时,为提高存取速度,会先把变量读取到一个缓存中,当以后再取变量值时,就直接从缓存中取值,当变量值在本线程里改变时,会同时把变量的新值复制到该缓存中,以便保持一致。

volatile是一个类型修饰符( type specifier ),它是被设计用来修饰被不同线程访问和修改的变量。被volatile类型定义的变量,系统每次用到它时都是直接从对应的内存当中提取,而不会利用缓存。在使用了volatile修饰成员变量后,所有线程在任何时候所看到变量的值都是相同的。

需要注意的是,由于volatile 不能保证操作的原子性,因此,一般情况下volatile不能代替sychronized。此外,使用volatile 会阻止编译器对代码的优化,因此会降低程序的执行效率。所以,除非迫不得已,否则,能不使用volatile就尽量不要使用volatile。

11.i++和++i区别

两种自增方式,前置和后置,即++i和i++,他们的不同点在于i++在程序执行完毕后自增,而++i在程序开始执行前进行自增

12.如何实现无符号数的右移操作

Java提供了两种右移运算符:“>>”和“>>>”。其中,“>>”被称为有符号右移运算符,“>>”被称为无符号右移运算符,它们的功能是将参与运算的对象对应的二进制数右移指定的位数。二者的不同点在于“>”在执行右移操作时,若参与运算的数字为正数,则在高位补0;若为负数,则在高位补1。而“>>>”则不同,无论参与运算的数字为正数或为负数,在执行运算时,都会在高位补0。

此外,需要特别注意的是,在对char、byte、short等类型的数进行移位操作前,编译器都会自动地将数值转化为 int类型,然后才进行移位操作。由于 int 型变量只占4Byte (32 bit) ,因此当右移的位数超过32 bit 时,移位运算没有任何意义。所以,在Java语言中,为了保证移动位数的有效性,以使右移的位数不超过32 bit,采用了取余的操作,即使a >>n等价于a >>( n% 32),示例如下:

public class test {
    public static void main(String[] args) {
        int i=-4;
        System.out.println("------>>int"+i);
        System.out.println("移前二进制:"+Integer.toBinaryString(i));
        i>>=1;
        System.out.println("移后二进制:"+Integer.toBinaryString(i));
        System.out.println("------>>int"+i);
    }
}

image

13.字符串创建与存储的机制

字符串的声明和创建主要有如下两种情况:

1.对于 String s1=new String("abc"),与String s2=new String("abc")语句,存在两个引用对象s1,s2,两个内容相同的字符串abc,他们在内存中的地址是不同的,只要用到new总会生成新的对象。

2.对于 String s1="abc" 与 String s2="abc"语句,在JVM中存在一个字符串常量池,其中保存很多String对象,并且可以被共享,s1,s2引用的是同一个常量池中的对象,当创建一个字符串常量时,会先在字符串常量池中查找是否已经有相同的字符串被定义,判断依据是String类的equals()方法的返回值,若已经定义,则直接获取对象的引用,若没有定义,则先创建这个对象,把它加到字符串常量池,再将它的引用返回。

image

image

public class Test {
    public void test() {

        String str1 = "abc";
        String str2 = new String("abc");
        System.out.println(str1 == str2);

        String str3 = new String("abc");
        System.out.println(str3 == str2);

        String str4 = "a" + "b";
        System.out.println(str4 == "ab");

        final String s = "a";
        String str5 = s + "b";
        System.out.println(str5 == "ab");

        String s1 = "a";
        String s2 = "b";
        String str6 = s1 + s2;
        System.out.println(str6 == "ab");

        String str7 = "abc".substring(0,2);
        System.out.println(str7 == "ab");
        
        String str8 = "abc".toUpperCase();
        System.out.println(str8 == "ABC");


        String s5 = "a";
        String s6 = "abc";
        String s7 = s5 + "bc";
        System.out.println(s6 == s7.intern());
    }
}

分析1

String str1 = "abc";
System.out.println(str1 == "abc");
  1. 栈中开辟⼀块空间存放引⽤str1,

  2. String池中开辟⼀块空间,存放String常量"abc",

  3. 引⽤str1指向池中String常量"abc",

  4. str1所指代的地址即常量"abc"所在地址,输出为true

分析2

String str2 = new String("abc");
System.out.println(str2 == "abc");
  1. 栈中开辟⼀块空间存放引⽤str2,

  2. 堆中开辟⼀块空间存放⼀个新建的String对象"abc",

  3. 引⽤str2指向堆中的新建的String对象"abc",

  4. str2所指代的对象地址为堆中地址,⽽常量"abc"地址在池中,输出为
    false

分析3

String str2 = new String("abc");
String str3 = new String("abc");
System.out.println(str3 == str2);
  1. 栈中开辟⼀块空间存放引⽤str3,

  2. 堆中开辟⼀块新空间存放另外⼀个(不同于str2所指)新建的String对象,

  3. 引⽤str3指向另外新建的那个String对象

  4. str3和str2指向堆中不同的String对象,地址也不相同,输出为false

分析4

String str4 = "a" + "b";
System.out.println(str4 == "ab");
  1. 栈中开辟⼀块空间存放引⽤str4,

  2. 根据编译器合并已知量的优化功能,池中开辟⼀块空间,存放合并后的String常量"ab",

  3. 引⽤str4指向池中常量"ab",

  4. str4所指即池中常量"ab",输出为true

分析5

final String s = "a";
String str5 = s + "b";
System.out.println(str5 == "ab");

步骤: 同4

分析6

String s1 = "a";
String s2 = "b";
String str6 = s1 + s2;
System.out.println(str6 == "ab");
  1. 栈中开辟⼀块中间存放引⽤s1,s1指向池中String常量"a",

  2. 栈中开辟⼀块中间存放引⽤s2,s2指向池中String常量"b",

  3. 栈中开辟⼀块中间存放引⽤str6,

  4. s1 + s2通过StringBuilder的最后⼀步toString()⽅法还原⼀个新的String对象"ab",因此堆中开辟⼀块空间存放此对象,

  5. 引⽤str6指向堆中(s1 + s2)所还原的新String对象,

  6. str6指向的对象在堆中,⽽常量"ab"在池中,输出为false

14.管理文件和目录的类

对文件或目录进行管理与操作在编程中有非常重要的作用,java提供了一种非常重要的类(File)来管理文件和文件夹,通过类不仅能查看文件或目录的属性,还可以实现对文件或目录的创建,删除与重命名等操作,下面介绍File类的常用方法
image

Q:如何列出某个目录下的所有文件和目录?

package com.basic.file;

import java.io.File;

public class OutFileInDirect {
    public static void main(String[] args) {
        File file=new File("C:\\downloads");
        if (! file.exists()){
            System.out.println("目录不存在");
            return;
        }
        File[] filelist=file.listFiles();
        for (File file1 : filelist) {
            if (file1.isDirectory()){
                System.out.println("Directory is:"+file1.getName());
            }else {
                System.out.println("File is:"+file1.getName());
            }
        }
    }


}

15.什么是Java Socket

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端成为一个Scoket,也称为套接字,用来实现不同虚拟机或不同计算机之间的通信。在Java中,Scoket可分为两种类型,面向连接的Scoket通信协议(Tcp,Transmission,传输控制协议)和面向无连接的Scoket通信协议(UDP,用户数据报协议),任何一个Scoket都是由IP地址和端口号唯一确定的

基于TCP的通信过程如下:首先Server(服务端)Listen(监听)指定的某个端口是否有连接请求,;其次Client(客户端)向Server(服务端)发出Connect(连接)请求;最后Server端向Client端发回Accept(接受)消息,一个连接就建立起来了,回话随即产生。

Scoket的生命周期可以分为三个阶段:打开Scoket,使用Scoket收发数据和关闭Scoket,在Java中,可以使用ServerScoket来作为服务端,Scoket作为客户端来实现网络通信。

Q:用Scoket实现客户端和服务端的通信,要求客户发送数据后能够回显相同的数据

创建一个java服务端代码

package com.basic.scoket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        BufferedReader br=null;
        PrintWriter pw=null;

        try {
            ServerSocket server=new ServerSocket(2000);
            Socket socket=server.accept();
            //获取输入流
            br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //获取输出流
            pw=new PrintWriter(socket.getOutputStream(),true);
            String s=br.readLine();
            pw.println(s);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            br.close();
            pw.close();
        }
    }
}

创建一个java客户端代码

package com.basic.scoket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws IOException {
        BufferedReader br=null;
        PrintWriter pw=null;

        try {
            Socket socket=new Socket("localhost",2000);
            //获取输入输出流
            br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            pw=new PrintWriter(socket.getOutputStream(),true);
            //向服务器发送数据
            pw.println("hello");
            String s=null;
            while (true){
                s=br.readLine();
                if(s!=null){
                    break;
                }
            }
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            br.close();
            pw.close();
        }
    }
}

启动服务端,运行客户端,客户端会把从服务端发过来的hello打印出来。

16.JVM加载class文件的机制是什么

Java语言是一种具有动态解释型的语言,了(class)只有被加载到JVM中才能运行,当运行指定程序时,JVM会将编译时产生的.class文件按照一定的需求和规则加载到内存中,并组织成为一个完整的java程序。这个过程是由类加载器来完成的(ClassLoader),具体来说就是由ClassLoader和它的子类来完成的,类加载器本身也是一个类,其实质就是把类文件从硬盘读取到内存中。

类的加载方式分为隐式和显式加载两种,隐式加载是程序在使用new等方式创建对象时,会隐式的调用类加载器把对应的类加载到JVM中。显式加载指的是直接通过调用class.forName()方法来把所需的类加载到JVM.

在java中,类的加载是动态的,并不会一次将所有的类一次性加载后再运行,而是保证程序运行的基础类(如基类)完全加载到JVM中,其他类则在需要时再加载。在java中,可以将类分为三类:系统类,扩展类,和自定义类。java针对这三种不同的类型提供了三种类型的类加载器,关系如下:

image
image

以上三个类是如何协调工作来完成了类的加载的呢?是通过委托的方式实现的,就是当有类需要被加载时,类加载器会请求父类来完成载入工作,父类会使用自己的搜索路径来搜索需要被载入的类,如果搜索不到,才会由子类按照其搜索路径来搜索待加载的类

public class TestLoader {
    public static void main(String[] args) {
        //调用类加载器
        ClassLoader clApp = TestLoader.class.getClassLoader();
        System.out.println(clApp);
        //调用上一层Class加载器
        ClassLoader clExt = clApp.getParent();
        System.out.println(clExt);
        //调用根部Class加载器
        ClassLoader clBoot = clExt.getParent();
        System.out.println(clBoot);
    }
}

image

上述可以看出,TestLoader类是由AppClassLoader来加载的,BootStrap Loader是用C++语言来实现的,因此在Java语言是看不到的,所以会输出null。

17.什么是GC

垃圾回收(GC)是一个非常重要的概念,它的主要作用是回收程序中不在使用的内存。在C/C++程序开发时,开发人员必须非常仔细的管理好内存的分配和释放,Java语言提供了垃圾回收器来自动的检测对象的作用域,自动的把不再使用的内存释放掉,垃圾回收器要负责完成3项任务:分配内存,确保引用对象的内存不被错误的回收,回收不再被引用的对象的内存空间。

对对象而言,如果没有任何变量去引用他,那么该对象将不可能被程序访问,因此可以被认为是垃圾信息,可以被回收,只要有一个以上的变量引用该对象,该对象就不会被垃圾回收。

对垃圾回收器来说,它使用有向图来记录和管理堆内存中的对象,通过这个有向图来识别哪些对象是“可达的”(有引用变量引用它就是可达的),哪些对象时“不可达的”(没有引用变量引用它),所有不可达对象都是可被垃圾回收的。示例:
image

上述代码在执行到i2=i1时,内存的引用关系:image
垃圾回收器遍历上述有向图时,资源2所处的内存是不可达的,垃圾回收器认为这块内存不在被使用,就会回收该块内存空间。

18.Java内存泄漏问题

内存泄漏是指一个程序不再使用的变量或对象还在内存中占用空间,在Java中提供了垃圾回收器来回收不再使用的对象,既然有垃圾回收器来负责回收垃圾,那么是否还存在内存泄漏呢?

在Java中,判断一个内存空间是否复合垃圾回收的标准有两个:第一,给对象赋予空值,第二,给对象赋予新值,重新分配内存空间。一般来讲,内存泄漏主要有两种情况:一是在堆中申请的空间没有被释放;二是对象已不再被使用,但还在内存中保留着。垃圾回收器能有效地解决第一种情况,第二种情况垃圾回收机制无法保证不在使用的对象会被释放,因此Java中的内存泄漏主要指第二种情况。

eg:

Vector v=new Vector();
        for(int i=0 ; i<10 ; i++){
            Object o=new Object();
            v.add(o);
        }

上述例子中,将不断创建的对象加到Vector对象中,退出循环后,o的作用域将会结束,但由于v在使用这些对象,因此垃圾回收器无法回收这些对象,因此造成内存泄漏。

在java中,引起内存泄漏的原因有很多,主要有一下几个方面的内容:

1,静态集合类,如HashMap,Vector。如果这些容器为静态的,他们的生命周期和程序一致,那么容器中的对象在程序结束之前将不能被释放,会造成内存泄漏。

2、各种链接,如数据库连接,网络连接,IO连接等,在操作数据库时,首先建立与数据库的连接,当不再使用时,会调用close()方法来释放与数据库的连接,只有连接被关闭后,垃圾回收器才能回收对应的对象,否则在访问数据库的过程中,对Connection,Statement,ResuleSet不显示的关闭,会造成成大量的对象无法被回收,从而造成内存泄漏。

3,监听器,在释放对象的同时没有相应的删除监听器,也可能造成内存泄漏。

4,变量不合理的作用域,一般而言,如果一个变量定义的作用范围大雨其使用范围,很可能导致内存泄漏

5,单例模式也可能在成内存泄漏,单例对象以静态变量的方式存储,因此它在整个JVM生命周期都存在。

19.Java中的堆和栈有什么区别

堆和栈都是内存中存放数据的地方,基本数据类型的变量以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放,而引用类型的变量其内存分配在堆上或常量池中,需要通过new等方式创建。

栈内存主要用来存放基本数据类型与引用变量,栈内存管理是通过压栈和弹栈操作来完成的,以栈帧为基本单位来来管理程序的调用关系,每当有函数调用时,都会通过压栈方式创建新的栈帧,调用结束后通过弹栈的方式释放栈帧。

堆内存主要存放运行时对象,一般来讲,通过new关键字创建出来的对象都存放在堆内存中,。由于JVM是基于堆栈的虚拟机,每个Java程序都运行在一个单独的JVM实例上,每个实例唯一对应一个堆,一个Java程序内的多个线程也运行在用一个JVM实例上,这些线程会共享堆内存,因此多线程在访问堆中的数据时要对数据进行同步。+

从堆和栈的功能及作用来比较,堆主要用来存放对象的,栈主要用来执行程序的,相较于堆,栈的存取速度更快,但栈的大小和生存期必须是确定的,因此缺乏一定的灵活性,而堆可以在运行时动态分配内存,生存期不用提前告诉编译器,但这也导致其存取速度的缓慢。

堆和栈的存储如下例:
image

程序进入main方法后,数据的存储关系如:
image
由于i为基本类型的变量,因此存储在栈空间中,而r为对象的引用变量,也被存储在栈空间中,实际的对象存储在堆空间中,当main方法退出后,存储在栈中的i和r通过压栈和弹栈操作将会在栈中被回收,而存储在堆中对象会被垃圾回收器来自动回收。

20,ArrayList,Vector和LinkedList有什么区别

他们都存在java.util包中,均为可伸缩数组。

ArrayList和Vector都会在内存中开辟一块连续的空间来存储,因此支持使用下标来访问,同时索引数据的速度比较快,但是在插入元素时需要移动容器中的元素,因此插入操作较慢,。ArrayList和Vector都有一个初始化的容量大小,当存储的元素超过这个大小时就要动态的扩充他们的空间,为了提高效率,每次扩容不是简单的扩充一个元素,而是一次增加多个存储单元。Vector默认扩充为原来的两倍(可以设置),ArrayList为原来的1.5倍(不可设置)。

ArrayList和Vector最大的区别就是synchronization(同步的使用),没有一个ArrayList的方法是同步的,Vector的绝大多数方法都是直接或间接同步的,因此ArrayList不是线程安全的,Vector是线程安全的,正式由于Vector提供了线程安全的机制,其性能上略差与ArrayList

LinkedList采用双向列表来实现的,对数据的索引需要从头开始,因此用于随机访问效率较低,但是插入元素时不需要移动数据,插入效率较高,同时,LinkedList是非线程安全的容器。

在实际使用时,当对数据的主要操作Wie索引或者只在集合的末端增加,删除元素时,使用ArrayList或者Vector,当在制定位置进行插入或删除时,使用LinkedList效率较高,当在多线程中使用容器时(多个线程同时访问该容器),选用Vector较为安全。

21,HashMap,HashTable,TreeMap和WeakHashMap有哪些区别

HashMap是一个最常用的Map,根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度HashMap和HashTable都采用hash法进行索引,因此有许多相似之处,他们主要有如下的一些区别:

1.HashMap是HastTable的轻量级实现(非线程安全),HashMap允许空(null)键值(key)(最多只允许一条记录的键为null),而HashTable不允许。

2.HashMap把HashTable的contains方法去掉了,改成containsvalue和containskey,因为contains方法容易让人误解。HashTable继承Dictionary类,而HashMap是java1.2引进的Map Interface的一个实现。

3.HashTable是线程安全的,而HashMap不支持线程同步,所以线程不安全,多个线程访问HashTable时,开发人员不需要对他进行线程同步

4.HashTable中,hash数组的默认大小是11,增加的方式是old*2+1,在HashMap中,hash数组的默认大小是16,而且一定是2的倍数、

TreeMap实现了SortMap,能够把它保存的记录按照键值排序,因此取出来的是排序好的元素,

WeakHashMap与HashMap类似,不同之处在于WeakHashMap采用“弱引用”的方式,只要WeakHashMap中的key不再被外部引用,他就可以被垃圾回收器回收,而HashMap中的key采用“强引用”的方式,当HashMap中的key没有被外部引用时,只有这个key从HashMap中删除,才能被垃圾回收器回收。

Q1:在HashTable上下文中,同步指的是什么?
同步意味着一个时间点只能有一个线程可以修改hash表,任何线程在执行HashTable的更新操作前都需要获取对象锁,其他线程等待锁的释放。

Q2:如何实现HashMap的同步?
HashMap可以通过 Map m = Connections.synchronizedMap(new HashMap())来达到同步的效果,具体而言,该方法返回一个同步的Map,该Map封装了底层的HashMap的所有方法,使得底层的HashMap即使在多线程也是安全的。

22.用自定义作为HashMap或HashTable的Key值需要注意那些问题?

当用自定义的类的对象做为HashMap的key时,会给人造成一种假象——key是可以重复的,示例:
image
image
运行结果为:
image

表面上看:向HashMap中添加的两个键值的key值是相同的,为啥后面添加的没有覆盖前面的value呢?HashMap添加 <k,v> 元素的过程:首先,调用key的 hashCode()方法生成一个 hash 值h1,如果这个h1在在HashMap中不存在,则将<k,v>添加到HashMap中;如果h1已经存在,那么找出HashMap中所有 hash 值为h1的key,再调用key的 equals()方法判断添加的key值是否与已经存在的key值相同,如果 equals()方法返回true,则说明当前添加的key已经存在,那么HashMap就会使用新的value值覆盖掉旧的value值;如果 equals()方法返回false,说明新增加的key在HashMap中不存在,会在HashMap中创建新的映射关系。当新增加的hash值在HasMap中已经存在,就会产生冲突,处理冲突的方法有开放地址法,再hash法,链地址法等,HashMap使用链地址法解决冲突,操作方法如图:
image

从HashMap中查找元素时,首先调用的是key的 hashCode()方法得到对应的hash值h,这样就可以确定键为key的所有值存储的首地址,如果h对应的key有多个,遍历所有的key,调用key的equals()方法来判断key的内容是否相等。只有当equals()方法返回true时,对应的value才是正确的结果。

多线程

23,什么是多线程,它与进程有什么区别?为什么要使用多线程?

线程是指能够执行程序代码的一个执行单元,在Java中,线程有四种状态:运行,就绪,挂起和结束。

进程是指一段正在执行的程序,线程有时也被称为轻量级线程,是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序内存空间(代码段,数据段,堆空间)及一些进程级的资源,例如打开的文件,但是各个线程拥有自己的栈空间,进程和线程的对比关系如图:
image

在操作系统级别上,程序的执行都是已进程为基本单位的,而每个进程中又有多个线程互不影响的并发执行,为什么要使用多线程呢?

1.可以减少程序的响应时间。在单线程下(单线程指的是在程序执行的过程中只有一个有效地草最序列,不同操作之间有明确的执行先后顺序)的情况下,如果某个操作很耗时,或陷入长时间等待,此时程序不会响应鼠标,键盘操作,在多线程下可以把这个耗时的线程分配到一个单独的线程去执行,是程序具备更好的交互性。

2.与进程相比,线程的创建和开销更小。

3.多CPU或多核计算机本身就具备执行多线程额的能力,如果使用单线程将无法重复利用计算机资源,造成资源的巨大浪费

4.使用多线程能简化程序的结构。

24.同步和异步有什么区别

多个线程在对同一数据进行写操作时,当线程A要使用某个资源时,如果这个资源正在被资源B使用,同步机制就会让A一直等待下去,直到线程B结束对该资源的使用,线程A才能使用这个资源,同步机制能保证资源的安全。

要想实现同步操作,必须获得每个线程对象的锁,获得它能保证在同一时刻只能有一个线程进入临界区(访问互斥资源的代码块),并且在这个锁被释放前,其他线程不能进入临界区,,若其他线程想获得该对象的锁,只能进入等待队列等待。只有拥有该对象的锁退出临界区时,锁才会被释放等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。

Java中提供了synchronized关键字来实现同步,但该方法是以很大的系统开销为代价的,有时甚至会造成死锁,所以同步控制并非越多越好,要尽量避免无所谓的同步控制,实现同步的方式有两种:一是同步代码块来实现同步,二是利用同步方法来实现同步。

异步在进行输入输出时,不必关心其他线程的状态,也不必等到输入输出处理完毕才返回,当应用程序调用一个需要花费很长时间来执行的方法,并且不希望等待程序返回时,应该使用异步编程,异步能够提高程序的运行效率。

25.如何实现java多线程

1.继承Thread类,重写run方法

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

public class ThreadTest extends Thread {
    @Override
    public void run() {
        System.out.println("thread body");
    }
}

public class MainTest {
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
    }
}

2.实现runable接口,并实现该接口的run方法
主要步骤:

1.实现Runable接口,实现run()方法。

2.创建Thread对象,用实现Runable接口的对象作为参数实例化该对象。

3.调用Thread的start()方法。

public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("thread body");
    }
}

public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread t = new Thread(myThread);
        t.start();
    }
}

不管是继承Thread类还是实现Runable接口来实现多线程的方法,最终还是通过Thread对象的API来控制线程。

26,run()方法和start()方法有什么区别

通常,系统通过调用线程类的start()方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM通过调用线程类的run()方法来完成实际的操作,当run()方法结束后,此线程就会终止。

如果直接调用run()方法,就会被当做一个普通的函数,程序中任然只有主线程这一个线程,也就是说start()方法能异步调用run()方法,但直接调用run()方法却是同步的,也就无法达到多线程的目的。由此可知,只有调用start()方法才能真正达到多线程目的。

例:
image
image
image

执行结果为:
image

从test1看,线程t1是在test1方法结束后才执行的,语句不需要等待t1.start()运行结果就可以执行,因此在test1方法中调用start()方法是异步的,所以main线程与t1线程是异步执行的。从test2看,调用t1.run()是同步的。

27.多线程同步的实现方法有哪些?

1.synchronzied关键字

java中每个对象都有一个对象锁与之关联,该锁表明对象在任何时候只能被一个线程所拥有,当一个线程调用对象的一段synchroized代码时,首先获取对象锁,再去执行代码,最后释放锁。
synchronized关键字主要有两种用法(synchronized方法和synchronized块),还可以作用与静态方法,类或某个实例,但都对程序的效率有很大影响。

1),synchronized方法,在方法声明前加入synchronized关键字:

public synchronized void ThreadAccess();
只要把多线程对类的需要被同步的资源放到ThreadAccess()方法中,就能保证这个方法在同一时刻只能内一个线程访问,从而保证安全性。但是当方法体规模非常大时,会大大影响程序的运行效率,为了提高程序的效率,提供了synchronized块

2),synchronized块既可以把任意代码块声明为synchronized,也可以指定上锁的对象。用法如下:

synchronized(syncObject){
//访问syncObject的代码块
}

2.wait()方法和notify()方法
当使用synchronized来修饰某个共享资源的时候,如果线程A在执行synchronized代码块,另一个线程A2也要同时执行同一对象的同一synchronized代码块时,线程A2要等待线程A1执行完成后才能继续执行。这种情况下可以使用wait()方法和notify()方法。
在synchronized代码执行期间,线程可以调用对象的wait()方法来释放对象锁,进入等待状态,并且可以调用notify()或notifyAll()通知其他正在等待的线程,notify()仅唤醒一个线程(等待队列的第一个线程),并允许它去获得锁。notifyAll()会唤醒所有等待这个对象的线程,并允许他们去获得锁(不是让所有的线程都或得到锁,而是让他们去竞争)。

3.Lock
JDK 5新增加了Lock接口以及它的一个实现了类ReentranLock(重入锁),它2提供了如下方法来实现线程的同步:
1),lock()以阻塞的方式获取锁,也就是说,如果获得了锁,立即返回,如果别的线程持有锁,当前线程等待,直到获取到锁后返回。
2),tryLock(),以非阻塞的方式获取锁,只是尝试性的获取锁,如说获取到,立即返回true,否则返回false。
3),tryLock(long timeout,TimeUnit unit),如果获取到了锁,立即返回true,否则会等待参数给定的时间单元,等待过程中获取到锁返回true,否则返回false
4),lockInterruptibly()

28.sleep()方法和wait()方法投什么区别?

1.原理不同

sleep()是Thread类的静态方法,是线程控制自身流程的,会使线程暂停执行一段时间,把执行机会给其他线程,及时时间一到,会自动苏醒;wait()方法是Object类的方法,用于线程间的通信,这个方法会使拥有该对象锁的线程等待,直到其他线程调用notify()方法才醒来。

2.对锁的处理机制不同

sleep()方法主要作用让线程暂停执行一段时间时间一到自动恢复,不涉及线程间的通信问题,因此调用sleep()方法不会释放锁。而调用wait()方法后线程会释放它所占用的锁,从而使线程所在对象中的其它synchronized数据可以被别的线程使用。

3.使用区域不同
由于wait()方法的特殊意义,必须放在同步控制方法或同步语句块中使用,而sleep()方法可以放在任何地方使用。

由于sleep()方法不会释放锁标志,因此容易导致死锁问题的发生,一般推荐使用wait()方法

29.终止线程的方法有那些?

Java中可以使用stop()和suspend()方法来终止线程的执行。Thread.stop()终止线程时,会释放已经锁定的所有监事资源。如果当前任何一个受这些监视资源的对象处于一个不一致的状态,其他线程将会“看”到这个不一致的状态,可能会导致程序执行的不确定性。调用suspend()方法容易发生死锁(两个或两个以上的线程争夺资源而造成的一种互相等待的状态,若无外力作用,它们将都无法推进),由于调用suspend()方法不会释放锁,如果用一个suspend挂起一个有锁的线程,那么在锁恢复之前不会被释放,如果调用suspend()方法,会试图取得相同的锁,程序就发生死锁。鉴于以上两种不安全的方法,Java已经不建议使用以上两种方式来终止线程了。

一般建议的方法是让线程自行结束进入dead状态。一个线程进入dead状态,即执行完run()方法。在实现时,可以通过设置一个flag标志来控制循环是否执行,通过这种方式来让线程离开run()方法从而终止线程。
image

30.synchronized和Lock有什么区别?

1。用法不一样:synchronized即可加载方法上,也能加在特点代码块中,而Lock需要显示的指定起始位置和终止位置;synchronized是交给JVM托管的,而Lock的锁定是通过代码来实现的。有比synchronized更精确的线程语义。

2.性能不一样:JDK 5增加了一个Lock接口的实现类ReentrantLock,在资源竞争不是很激烈的情况下,synchronized的性能要优于ReentrantLock,但在资源竞争非常激烈的情况下,synchronized的性能下降的非常快,而ReentrantLock的性能基本不变。

3.锁机制不一样:synchronized获取锁和释放的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动释放。不会因为出了异常而导致锁没有被释放而到导致死锁问题的发生。而Lock需要开发人员手动释放,并且必须在finally块中释放,否则会引起死锁问题的发生。此外Lock的teyLock()还可以以非阻塞的方式去获取锁。

package com.basic.thread.synctest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SyncTest {
    private volatile int value=0;
    Lock lock=new ReentrantLock();
    public synchronized void addValueSync(){
        this.value++;
        System.out.println(Thread.currentThread().getName()+":"+value);
    }

    public void addValueLock(){
        try {
            lock.lock();
            value++;
            System.out.println(Thread.currentThread().getName()+":"+value);
        } finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) {
        SyncTest st = new SyncTest();
        final Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0;i<5;i++){
                    st.addValueSync();
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0;i<5;i++){
                   st.addValueLock();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
//        t1.start();
        t2.start();
    }

}

31.JDBC处理事务

一个事务是是由一条或多条对数据库操作的sql语句所组成的一个不可分割的单元,只有当事务中的所有操作都完成了,整个事务才会被提交给数据库。

Q:JDBC有哪些事务隔离级别?

为了解决多个线程请求相同数据的问题,事务之间通常会用锁相互隔开。如今大多数数据库支持不同的锁,因此JDBC API支持不同的事务类型,它们由Connection对象指派或确定。JDBC定义了五种事务隔离级别:

1.不支持事务

2.未提交读:说明在提交前一个事务可以看到另一个事务的变化,这样读“脏数据”,不可重复读和虚读都是允许的。

3.已提交读:说明读取未提交的数据是不允许的。这个级别任然允许不可重复读和虚读产生。

4.可重复读:说明事务能够保证再次读取相同的数据而不会失败,但虚读任会出现。

5.可序列化:是最高的事务级别,它防止读“脏数据”,不可重复读和虚读。
image
image

事务隔离级别越高,为避免冲突所花的精力也就越多。可以通过Connection对象的coon.setTransationLevel()方法来设置隔离级别。通过coon.getTransactionLevel()来确定当前事务的隔离级别。

32.session和cookie区别

1.cookie信息保存在客户端浏览器上,session信息保存在服务器上。

2.cookie安全性不够。信息存放在客户端,其他人能获取到存放在本地的cookie,session信息存放在客户端,较为安全。

3.cookie性能更高,session会在一定时间内保存到服务器,当访问量增加时,会降低服务器的性能。

4.单个cookie的数据不能超过4kb,很多浏览器限制一个站点最多只能保存20个cookie,session不存在这个问题。

推荐阅读