Java基本知识

来源:互联网 发布:数据如何脱敏 编辑:程序博客网 时间:2024/05/22 10:46

Java的多态

多态是面向对象语言的三大特性之一。多态主要有两种形式,一种是编译时多态,另一种是运行时多态。

编译时多态的实现就是对同名方法的重载(Overload)。编译器会根据同名方法的形参个数,形参类型,以达到在编译时就能区分不同的方法。

运行时多态的实现就是对方法的重写(Override)。子类继承父类并重写了父类的某些非静态方法,然后把子类对象赋值给父类引用,父类引用在调用方法时并不能在编译期就确定调用的是哪一个类的方法。若调用的方法是子类重写过的,那么调用的就是子类的方法;否则调用父类的方法。这都是在运行时才能确定的。若重写的是静态方法,那么调用的时候会调用父类的静态方法。

当父类和子类有同名变量时,父类引用访问同名变量时会访问父类的。静态方法也是如此。

Clone方法的作用

在Java中存在浅复制和深复制两种形式。浅复制就是使用“=”,将一个地址赋值给一个引用变量。例如:

        List list1=new ArrayList<Integer>();        list1.add(1);        list1.add(2);        List list2=list1;//list1的地址赋值给list2        System.out.println(list1);//[1, 2]        System.out.println(list2);//[1, 2]        list2.add(3);        System.out.println(list1);//[1, 2, 3]

深复制使用“clone()”方法,在内存中新开辟一个空间,把内容填入该空间中。而“clone()”方法是Object的protected方法,必须要重写该方法才可以使用。同时,必须实现Cloneable接口才可以调用“clone()”方法。

A a1=new A(1, "a");//实现了Cloneable接口,并重写clone()方法A a2=(A)a1.clone();System.out.println(a1);//A [id=1, name=a]System.out.println(a2);//A [id=1, name=a]a2.setId(3);System.out.println(a1);//A [id=1, name=a]System.out.println(a2);//A [id=3, name=a]

Java内部类

内部类是指讲一个类定义内置于另一个类定义之中。它作用是:

  1. 将类定义在另一个类之中,可以隐藏内部实现,达到更好的封装性
  2. 内部类可以访问外部类的所有方法和变量,使得代码编写更加方便
  3. 通过多个内部类继承不同的类,可以达到多继承的效果
  4. 外部类继承一个类,内部类实现接口,可以避免修改接口而实现同一个类中两种同名方法的调用

Java接口与抽象类的区别

对比项 抽象类 接口 抽象方法修饰符 抽象方法可以是public、protected和default这些修饰符,同时必须加abstract修饰 接口的方法只能是public abstract 默认方法实现 抽象类中允许有默认方法实现,可以是private修饰 接口方法都是abstract的,不允许默认实现 子类实现 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 继承 可以继承一个类和实现多个接口 只能继承一个或多个其它接口 速度 它比接口速度要快 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 成员变量 可以是public,private,protected和default这些修饰符,与普通类没有区别 只能是public,static,final的变量,所以必须复制 构造方法 可以有构造方法,但不能实例化对象 没有构造方法

什么时候使用接口或抽象类?
1. 如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧。
2. 如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
3. 如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。

Java的io与nio的区别

对比项 io nio 面向形式 面向流 面向缓冲区 是否阻塞 阻塞 非阻塞 是否有选择器 无 有

Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。

Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。

Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

String,StringBuffer,StringBuilder的区别

String是不可变字符串,所定义的字符串保存在字符串池里。所以每次String a=”abc”时,都会先在池中查找是否存在“abc”,若存在则引用a直接指向该地址。同时,该字符串是不可变的。所以每次对字符串进行操作的时候,都会生成一个新的字符串。

而StringBuffer和StringBuilder是一个包含可变字符串的类,因为它们是预先申请一个缓冲区来存放字符串。当字符串长度超过缓冲区大小时,它会自动申请更大空间,已容纳更多的字符。StringBuffer和StringBuilder的唯一区别是,StringBuffer是线程安全的,StringBuilder是非线程安全的。

所以在有字符串拼接的时候,String会不断的生成新的字符串,这样不但会浪费空间,还会造成速度低下。下面有一个特殊的例子:

String str = "This is only a" + " simple" + " test";

这个字符串生成非常快,这是jvm的优化效果。它作了一个这样的处理:

String str = "This is only a simple test";

如果字符串是来自另外的String对象的话,速度就没那么快了

Java异常机制

这里写图片描述

所有异常都是Throwable类的子类,其下有两个子类,一个是Error,一个是Exception。

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

Exception:是程序本身可以处理的异常。

注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
通常,异常分为可检查异常和不可检查异常。

可检查异常:编译时必须处理的异常。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

不可检查异常:包括运行时异常(RuntimeException与其子类)和错误(Error)

在Exception类下,异常分为两种,运行时异常和编译异常。

运行时异常:都是RuntimeException类及其子类异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

编译异常:是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。

Java实现多线程

Java实现多线程有两种方法,一种是继承Thread类,一种是实现Runnable接口。

  • 继承Thread类:定义一个类继承Thread类,并重写run()方法。然后创建该类的对象,调用start()方法,启动线程。
  • 实现Runnable接口:定义一个类实现Runnable接口,并重写run()方法。然后创建该类对象并作为实参传入Thread的构造函数里,创建Thread类对象,调用start()启动线程。

两者区别:
(1)实现Runnable接口避免了单继承的局限性
(2)继承Thread类线程代码存放在Thread子类的run方法中,实现Runnable接口线程代码存放在接口的子类的run方法中;在定义线程时,建议使用实现Runnable接口,因为几乎所有多线程都可以使用这种方式实现

sleep()和wait()的区别

  1. sleep()方法是Thread的方法,wait()是Object的方法
  2. sleep()方法会释放资源不会释放锁,wait()方法会释放资源和锁,直到有其他线程使用notify()/notifyAll()把该线程唤醒
  3. sleep()方法是静态方法,在哪一个线程里调用则该线程就sleep。即使在a线程里调用了b的sleep方法,实际上还是a去睡觉。而wait()是Object类的非静态方法
  4. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

为什么wait()、notify()、notifyAll()这些用来操作线程的方法定义在Object类中

(1)这些方法只存在于同步中;
(2)使用这些方法时必须要指定所属的锁,即被哪个锁调用这些方法;
(3)而锁可以是任意对象,所以任意对象调用的方法就定义在Object中。

多线程之间通信

  1. synchronized关键字进行同步通信
  2. 使用wait,notify,notifyAll进行通信
  3. 使用Lock,Conditions进行通讯
  4. 使用管道通信。管道流只能实现单向发送,如果要两个线程之间互通讯,则需要两个管道流。管道流只能在两个线程之间传递数据。

如何停止线程

现在stop方法已经过时,不推荐使用,那么怎样停止线程呢?
1. 线程执行的是run()方法,要停止线程可以想办法终止run()方法。那么可以使用判断标志来结束方法。一般可以设置一个public的判断标志,然后在需要停止线程时,在其他地方设置该标志即可。
2. 但是若该线程处于阻塞状态(sleep,wait,join),则不能进行判断。这时候可以使用interrupt方法,抛出中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。

守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。守护线程是为其他线程服务的线程,例如有GC线程。当所有非守护线程退出后,守护线程也会退出。
在Java中可以设置守护线程,必须在调用start()方法前,使用thread.setDaemon(true)设置为守护线程。

yield()与sleep()的区别

yield():暂停当前正在执行的线程对象,并执行同级别的线程,若没有,则继续执行。
sleep():暂停当前正在执行的线程对象,并执行其他线程,不管级别。

ThreadLocal类

ThreadLocal是线程局部变量,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

ThreadLocal<T>();//创建线程局部变量T get();//返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本,值默认为null。void remove();//移除此线程局部变量的值。void set(T value);//将此线程局部变量的当前线程副本中的值设置为指定值T initialValue();//返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。

使用步骤:
1、在多线程的类中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
2、在类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
3、在类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

public class A extends Thread{    ThreadLocal<Integer> var=new ThreadLocal<>();    @Override    public void run() {        for(int i=0;i<10;++i){            System.out.println(getName()+" "+getVar());            var.set(i+1);        }    }    public Integer getVar() {        Integer result=(Integer)var.get();        if(result==null){            result=new Integer(1);            var.set(result);        }        return result;    }}

Set,Map中加入自定义对象

在Set,Map中要加入自定义对象时,最好要重写该对象类的equals和hashCode方法。因为在Set,Map中,先使用hashCode获取地址,再利用equals来判断是否存在该对象。

Arrays.asList(T… a)方法

该方法是一个static方法,它可以返回一个受指定数组支持的固定大小的列表。注意:

A:该方法将一个数组变成集合后,不可以使用集合的增删方法,因为数组的长度是固定的!如果增删,则发生UnsupportedOprationException(不支持操作异常)

Integer []aIntegers={new Integer(1),new Integer(2)};List<Integer> list=Arrays.asList(aIntegers);list.add(3);System.out.println(list);

报错:

Exception in thread "main" java.lang.UnsupportedOperationException    at java.util.AbstractList.add(AbstractList.java:148)    at java.util.AbstractList.add(AbstractList.java:108)    at Test.main(Test.java:9)

B:如果数组中的元素都是基本数据类型,则该数组变成集合时,会将该数组作为集合的一个元素出入集合

int []a={1,2,3};List<int[]> list=Arrays.asList(a);System.out.println(list);//[[I@33909752]

C:如果数组中的元素都是对象,如String,那么数组变成集合后,数组中的元素就直接转成集合中的元素

Integer []aIntegers={new Integer(1),new Integer(2)};List<Integer> list=Arrays.asList(aIntegers);System.out.println(list);//[1, 2]

基本类型的封箱与拆箱

基本类型 封装类型 byte Byte short Short int Integer long Long float Float double Double boolean Boolean char Character

装箱:自动将基本类型转变为对应的封装类型
拆箱:自动将封装类型转变为对应的基本类型

享元模式:
对于基本数据类型的整数,装箱成Integer对象时,如果该数值在一个字节内,(-128~127), 一旦装箱成Integer对象后,就把它缓存到磁里面,当下次,又把该数值封装成Integer对象时会先看磁里面有没有该对象,有就直接拿出来用,这样就节省了内存空间。因为比较小的整数,用的频率比较高,就没必要每个对象都分配一个内存空间。

Integer a = 1;Integer b = 1;System.out.println(a==b);//trueInteger c = 129;Integer d = 129;System.out.println(c==d);//false

虽然自动装箱拆箱很方便,但是还是需要注意正确声明变量类型。

Integer sum = 0; for(int i=1000; i<5000; i++){   sum+=i;}/*实际上:sum = sum.intValue() + i;Integer sum = new Integer(result);*/

这样会产生很多无用的对象,造成空间浪费和效率低下。

重写equals方法,为什么还要重写hashCode方法

Java里协定重写equals方法后,还要重写hashCode方法,以保证:
(1)a.equals(b)返回true,a.hashCode()==b.hashCode()也必须返回true。
(2)a.hashCode()==b.hashCode()返回false,a.equals(b)也必须返回false。

不重写时,equals方法都是比较是否指向同一块内存地址。一般来说,重写equals方法是为了判断两对象的值是否相等。而hashCode方法用在set,map中判断是否存在该对象。若只重写equals,那么两对象值相同。但没有重写hashCode,那么新建一个对象时,即使两对象值相同,但是hashCode不同,那么也是可以存入set中的。这样就不对了。所以要重写hashCode。