本文最后更新于 2025-03-06,文章超过7天没更新,应该是已完结了~

Java/面向对象 四个基本特性

1)封装:把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。

我们在类中编写的方法就是对实现细节的一种封装;我们编写的类就是对数据和数据操作的封装

可以说,封装就是隐藏一切可隐藏,只向外界提供最简单的编程接口。

2)继承:从已有类即父类得到继承信息并创建新类的过程。在Java中是单继承,一个子类只能有一个父类

好处:1.子类能自动继承父类的接口 2.创建子类的对象时,无需创建父类的对象

3)多态:

多态是同一个行为具有多个不同表现形式或形态的能力。

我们假设有基类Animal,两个Animal的派生类Cat和Dog。

我现在有块广告牌,想要输入什么动物就放什么动物的照片?如果没有多态,我是不是需要不断地进行判断?

那么有了多态,我们可以如下实现:

// 创建Animal类
class Animal{
	protected String name;	// 可被子类访问的name
	public Animal() {
		this.name = "Animal";
	}
	// 封装
	public String getName() {
		return this.name;
	}
}

class Cat extends Animal{
	Cat(){
		name = "Cat";
	}
}

class Dog extends Animal{
	Dog(){
		name = "Dog";
	}
}

public class Test {
	static public void board(Animal s) {		
		System.out.println(s.getName());
	}
	
	public static void main(String[] args) {
		Animal animal = new Animal();	//创建Animal对象
		Animal cat = new Cat();			//创建Cat对象 子类类型的指针赋值给父类类型的指针
		Animal dog = new Dog();			//创建Dog对象 子类类型的指针赋值给父类类型的指针
		// 三块广告牌
		board(animal);
		board(cat);
		board(dog);
		
	}
}

多态存在的三个必要条件

  1. 继承

  2. 重写

  3. 允许将子类类型的指针赋值给父类类型的指针,把不同的子类对象都当作父类来看。比如你家有亲属结婚了,让你们家派个人来参加婚礼,邀请函写的是让你爸来,但是实际上你去了,或者你妹妹去了,这都是可以的,因为你们代表的是你爸,但是在你们去之前他们也不知道谁会去,只知道是你们家的人。可能是你爸爸,可能是你们家的其他人代表你爸参加。这就是多态。

多态又分为 编译时多态和运行时多态。
编译时多态:比如重载,相同的方法有不同的参数列表,可以根据参数的不同,做出不同的处理。
运行时多态:比如重写,多个子类重写父类的方法,在运行期间判断所引用对象的实际类型,根据其实际类型调用相应的方法。

4)抽象:Java语言中,用abstract 关键字来修饰一个类时,这个类叫作抽象类。抽象类是它的所有子类的公共属性的集合,是包含一个或多个抽象方法的类。抽象类可以看作是对类的进一步抽象。在面向对象领域,抽象类主要用来进行类型隐藏。

JDK/JRE/JVM三者的关系

JVM

JVM:英文名称(Java Virtual Machine),就是我们耳熟能详的Java虚拟机。Java能够跨平台运行的核心在与JVM。

所有的java程序首先被编译为 .class的类文件,这种类文件可以在虚拟机上运行,也就是说class文件并不直接与机器的操作系统交互,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。

针对不同的系统有不同的jvm实现,但是同一段代码编译后的字节码是一样的。这就是Java能够跨平台,实现一次编写,多次运行的原因。

JRE

英文名称(Java Runtime Environment),就是Java运行时环境。Java程序必须在JRE中运行。JRE包含两个部分,JVM和Java核心类库。

JRE是Java的运行环境,并不是一个开发环境,所以没有包含任何开发工具,如编译器和调试器等。

如果你只是想运行Java程序,而不是开发Java程序的话,那么你只需要安装JRE即可。

JDK

英文名称(Java Development Kit),就是Java开发工具包,JDK目录下有JRE,也就是JDK中已经集成了JRE,不用单独安装JRE,另外JDK还有一些好用的工具,如 jinfo,jps,jstack等。

总结

JRE=JVM+Java核心类库

JDK=JRE+Java工具+编译器+调试器

面向对象和面向过程的区别

一、面向对象与面向过程的区别

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

可以拿生活中的实例来理解面向过程与面向对象,例如五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。

如果是面向对象的设计思想来解决问题。面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为

1、黑白双方

2、棋盘系统,负责绘制画面

3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

二丶面向对象和面向过程的优缺点

面向过程

优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。

缺点:没有面向对象易维护、易复用、易扩展

面向对象

优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护

缺点:性能比面向过程低

Java与C++的区别

  • Java是纯粹的面向对象语言,所有的对象都继承自java.lang.Object,C++兼容C,不但支持面向对象也支持面向过程

  • Java通过虚拟机从而实现跨平台特性,C++依赖特定的平台

  • Java没有指针,它的引用可以理解未安全指针,而C++具有和C一样的指针

  • Java支持自动垃圾回收,而C++需要手动回收

  • Java不支持多重继承,只能通过实现多个接口来达到相同的目的,而C++支持多重继承。

重写与重载

方法重载是指同一个类中的多个方法具有相同名字,但这些方法具有不同的参数列表,即参数的数量和参数类型不能完全相同。

方法重写是存在子父类之间的,子类定义的方法在父类中的方法有相同方法名字,相同的参数表和相同的返回类型。

方法重写规则:

  • 声明为final的方法不能被重写。

  • 声明为static的方法不能被重写,但是能够被再次声明。

  • 当需要在子类中调用父类的那个被子类重写的方法时,要使用super关键字。

方法重载规则:

  • 方法能够在同一个类中或者在一个子类中被重载。

  • 无法以返回值类型作为重载函数的区分标准。

Java的8大基本数据类型

通常的浮点型数据在不声明的情况下都是double型的,如果要表示一个数据时float 型的,可以在数据后面加上 "F" 。浮点型的数据是不能完全精确的,有时候在计算时可能出现小数点最后几位出现浮动,这时正常的。所以不建议用浮点型表示金额,建议用BigDecimal或者Long表示金额。

String.getBytes(encoding) 方法获取的是指定编码的byte数组表示。一个汉字通常gbk / gb2312 是两个字节,utf-8 是3个字节。

自动转换(隐式):无需任何操作。

强制转换(显式):需使用转换操作符:(type)。

将6种数据类型按下面顺序排列一下:

double > float > long > int > short > byte

如果从小转换到大,那么可以直接转换,而从大到小,或char 和其他6种数据类型转换,则必须使用强制转换。

扩展:十进制数用D表示,二进制用B表示,十六进制数用H表示,八进制用O表示

进行四则运算时:

a、所有的byte型、short型和char的值将被提升到int型

b、如果一个操作数是long型,计算结果就是long型

c、如果一个操作数是float型,计算结果就是float型

d、如果一个操作数是double型,计算结果就是double型

e、如果一个操作数是String型,计算结果就是String型

什么是值传递和引用传递?

  • 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

  • 所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

  • java中不存在引用传递,只有值传递。

第一个例子:基本类型
void foo(int value) {
    value = 100;
}
foo(num); // num 没有被改变

第二个例子:没有提供改变自身方法的引用类型
void foo(String text) {
    text = "windows";
}
foo(str); // str 也没有被改变

第三个例子:提供了改变自身方法的引用类型
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
    builder.append("4");
}
foo(sb); // sb 被改变了,变成了"iphone4"。

第四个例子:提供了改变自身方法的引用类型,但是不使用,而是使用赋值运算符。
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
    builder = new StringBuilder("ipad");
}
foo(sb); // sb 没有被改变,还是 "iphone"。

重点在第三个例子和第四个例子,第三个例子的图解

执行append("4")后

解释:builder接收的是iphone的参数地址,无法对参数地址进行改变。但是地址的内容是可以修改的。

第四个例子图解:

new StringBuilder("ipad")后

了解Java的包装类型吗?为什么需要包装类?

Java是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如在集合类中,我们是无法将int、double 等类型放进去的。因为集合的容器要求元素是Objcet类型。

为了让基本类型也具有对象的特征,就出现了包装类型。相当于将基本类型包装起来,使得它具有对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

自动装箱和拆箱

Java中基本数据类型与它们对应的包装类见下表:

原始类型

包装类型

boolean

Boolean

byte

Byte

char

Character

float

Float

int

Integer

long

Long

short

Short

double

Double

装箱:将基础类型转化为包装类型

拆箱:将包装类型转化为基础类型

当基础类型与它们的包装类有如下几种情况时,编译器会自动帮我们拆箱或装箱。

  • 赋值操作(装箱或拆箱)

  • 进行四则运算(拆箱)

  • 进行 >,<,==比较运行算(拆箱)

  • 调用equals进行比较(装箱)

  • 集合类添加基础类型数据时(装箱)

Integer x=1; //装箱 调用了Integer.valueOf(1)
int y=x; //拆箱 调用了x.intValue()

String,StringBuffer(安全)和StringBuilder(不安全)

可以看到String类是通过内部的一个char数组实现的,并且是final修饰的,说明value数组不可以指向其它对象。并且所有字段都是私有的,没有对外提供修改内部状态的方法,说明value数组不能改变。

String为什么要设计成不可变的?

  1. 线程安全。同一个字符串实例可以被多个线程共享,因为字符串不可变,本身时线程安全的。

  2. 支持hash映射和缓存。因为String的hash值经常会被使用到,比如作为Map的key,不可变使得hash值也不会变,不需要重新计算。

  3. 出于安全考虑。网络地址URL,文件路径path,密码通常是用Srting类型保存,假若String不是固定不变的,将会引起各种安全隐患。

  4. 适合字符串常量池优化。String对象创建之后,会缓存到字符串常量池中,下次创建同样的对象,直接返回缓存的引用。

既然我们的String是不可变的,它内部还有很多substring,replace, replaceAll这些操作的方法。这些方法好像会改变String对象?怎么解释呢?

其实不是的。我们每次调用replace等方法,其实会在堆内存中创建了一个新的对象。然后其value数组引用指向不同的对象。

String,StringBuffer和StringBuilder的区别?

StringBuilder只实现了两个接口Serializable、CharSequence,相比之下String的实例可以通过compareTo方法进行比较,其他两个不可以。

扩展:compareTo()

1.如果第一个字符和参数的第一个字符不等,结束比较,返回第一个字符的ASCII码差值。

2.如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至不等为止,返回该字符的ASCII码差值。 如果两个字符串不一样长,可对应字符又完全一样,则返回两个字符串的长度差值。

运行速度快慢为:StringBuilder > StringBuffer > String

 String最慢的原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。以下面一段代码为例:

运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢

StringBuffer与StringBuilder的共同之处:

1、都继成了AbstractStringBuilder这个抽象类,实现了CharSequence接口

2、其append方法都是 super.append(str),调用了父类AbstractStringBuilder的append(String str)方法

3、初始容量都是16和扩容机制都是"旧容量*2+2"

4、可以通过append、indert进行字符串的操作。

StringBuffer与StringBuilder的不同之处:

1、StringBuffer多线程安全的,内部使用synchronized进行同步;StringBuilder多线程不安全

3、StringBuffer比StringBuilder多了一个toStringCache字段,用来在toString方法中进行缓存,每次append操作之前都先把toStringCache设置为null,若多次连续调用toString方法,可避免每次Arrays.copyOfRange(value, 0, count)操作,节省性能。

扩展:Arrays.copyOfRange()的一点细节

相关的一些高频面试题:

String类的常用方法

  • indexOf() :返回指定字符的索引。

  • charAt() : 返回指定索引处的字符。

  • replace() : 字符串替换。

  • trim() : 去除字符串两端空白。

  • split() : 分割字符串,返回一个分割后的字符串数组。

  • getBytes() : 返回字符串的byte类型数组。

  • length() : 返回字符串长度。

  • toLowerCase : 将字符串转成小写字母。

  • toUpperCase : 将字符串转成大写字符。

  • substring() : 截取字符串。

  • equals() : 字符串比较。

Object常用方法有哪些?

Object常用方法有: toString() , equals() , hashCode() , clone() 等。

toString

默认输出对象地址。

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public static void main(String[] args) {
        System.out.println(new Person(18, "wzc").toString());
    }
    //output
    //me.tyson.java.core.Person@4554617c
}

可以重写toString方法,按照重写逻辑输出对象值。

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return name + ":" + age;
    }

    public static void main(String[] args) {
        System.out.println(new Person(18, "wzc").toString());
    }
    //output
    //wzc:18
}

equals

默认比较两个引用变量是否指向同一个对象(内存地址)

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
       this.age = age;
       this.name = name;
    }

    public static void main(String[] args) {
        String name = "wzc";
        Person p1 = new Person(18, name);
        Person p2 = new Person(18, name);

        System.out.println(p1.equals(p2));
    }
    //output
    //false
}

可以重写equals方法,按照age和name是否相等来判断

==:如果比较的是基本数据类型,比较的是值,如果是引用类型,比较的是引用地址

hashCode

将与对象相关的信息映射成一个哈希值,默认的实现hashCode值是根据内存地址换算出来。

public class Cat {
    public static void main(String[] args) {
        System.out.println(new Cat().hashCode());
    }
    //out
    //1349277854
}

clone

Java赋值是复制对象的引用,如果我们想要得到一个对象的副本,使用赋值操作无法达到目的。Object对象有个clone()方法,实现了对象中各个属性的赋值,但它的可见范围是protected的。

protected native Object clone() throws CloneNotSupportedException;

实体类实现clone方法的前提是:

  • 实现Cloneable接口,这是一个标记接口,自身没有方法,是一种约定。调用clone()方法时,会判断有没有实现Cloneable接口,没有实现Cloneable的话会抛异常CloneNotSupportedException。

  • 覆盖clone()方法,可见性提升为public。

public class Cat implements Cloneable {
    private String name;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat c = new Cat();
        c.name = "wzc";
        Cat cloneCat = (Cat) c.clone();
        c.name = "大哥";
        System.out.println(cloneCat.name);
    }
    //output
    //wzc
}

getClass

返回此Object的运行时类,常用于java反射机制。

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Person p = new Person("wzc");
        Class clz = p.getClass();
        System.out.println(clz);
        //获取类名
        System.out.println(clz.getName());
    }
    /**
     * class com.tyson.basic.Person
     * com.tyson.basic.Person
     */
}

wait

当前线程调用对象的wait()方法之后,当前线程会释放对象锁,进入等待状态。等待其他线程调用此对象的notify()/notifyAlloe醒或者等待超时时间wait(long timeout)自动唤醒。线程需要获取obj对象锁之后才能调用obj.wait()。

notify

obj.notify()醒在此对象上等待的单个线程,选择是任意性的。notifyAllo唤醒在此对象上等待的所有线程。

讲讲对象赋值,深拷贝和浅拷贝

直接赋值(引用赋值

public class User {

    private String name;

    private Integer age;

    private String sex;
}
 public static void main(String[] args){
        User user1 = new User();
        user1.setName("张三");
        user1.setAge(20);
        user1.setSex("男");

        //引用赋值
        User user2 = new User();
        user2 = user1;
        user2.setName("李四");
        user2.setAge(22);

        System.out.println("user1:"+user1);
        System.out.println("user2:"+user2);

    }

对象的直接赋值时将user1 的引用地址赋值给了user2,user2修改实例数据的时候是修改堆当中的数据,所以当user2修改完成后我们再查看user1的数据时看到的是修改后的数据。

对象拷贝

对象拷贝都是在堆内存当中重新分配一块内存并将原来的数据赋值到新的对象,但是由于一个对象可能存在对另外某个对象的引用,所以拷贝分为深拷贝和浅拷贝。

  • 浅拷贝:当一个对象引用别的对象时,对基本数据类型进行值传递,对引用对象进行引用地址拷贝。

  • 深拷贝:当一个对象引用别的对象时,对基本数据类型进行值传递,对引用对象则创建一个新的对象,并复制其内容。

举个例子:

  1. 一个对象不包含对别的对象的引用。浅拷贝:

//拷贝需要实现CloneAble接口,并重写clone方法
public class User implements Cloneable {

    private String name;

    private Integer age;

    private String sex;

    @Override
    public Object clone(){
        try {
            return super.clone();
        }catch (CloneNotSupportedException ignore){

        }
        return null;
    }
}
    public static void main(String[] args){
        User user1 = new User();
        user1.setName("张三");
        user1.setAge(20);
        user1.setSex("男");
        //浅拷贝
        User user2 = (User)user1.clone();
        user2.setName("李四");
        user2.setAge(22);

        System.out.println("user1:"+user1);
        System.out.println("user2:"+user2);
        System.out.println(user1==user2);
    }

结论:开辟新的内存空间,将原来对象基本数据类型进行值传递,修改user2不影响user1

  1. 一个对象包含了对别的对象的引用。使用浅拷贝:

public class Person {

    private String person;

}
public class User implements Cloneable {

    private String name;

    private Integer age;

    private String sex;

    private Person person;

    @Override
    public Object clone(){
        try {
            return super.clone();
        }catch (CloneNotSupportedException ignore){

        }
        return null;
    }
}
 public static void main(String[] args){
        User user1 = new User();
        user1.setName("张三");
        user1.setAge(20);
        user1.setSex("男");
        Person person = new Person();
        person.setPerson("morning!");
        user1.setPerson(person);
        //浅拷贝
        User user2 = (User)user1.clone();
        user2.setName("李四");
        user2.setAge(22);
        //修改引用对象的值,还是会修改到user1
        user2.getPerson().setPerson("afternoon!");

        System.out.println("user1:"+user1);
        System.out.println("user2:"+user2);

    }

结论:浅拷贝不会把该对象对其它对象的引用进行拷贝

对与浅拷贝来说,如果包含对别的对象的引用,在内存中结构如下:浅拷贝对象的引用还是指向原来的引用对象,并没有重新复制一份给到新的对象

  1. 深拷贝,将对象中对其它对象的引用也一起复制给新的对象

实现深拷贝的方式:

  1. 对对象进行序列化操作,在赋值给新对象的时候反序列化操作。

  2. 实现clone方式。(推荐

public class Person implements Cloneable{
    private String person;
    @Override
    public Object clone(){
        try {
            return super.clone();
        }catch (CloneNotSupportedException ignore){

        }
        return null;
    }
}

public class User implements Cloneable {

    private String name;

    private Integer age;

    private String sex;

    private Person person;

    @Override
    public Object clone(){
        try {
            //深拷贝
            User user = (User)super.clone();
            user.person = (Person)this.person.clone();
            return  user;
        }catch (CloneNotSupportedException ignore){

        }
        return null;
    }
 public static void main(String[] args){
        User user1 = new User();
        user1.setName("张三");
        user1.setAge(20);
        user1.setSex("男");
        Person person = new Person();
        person.setPerson("morning!");
        user1.setPerson(person);

        //深拷贝 对象已经实现clone方法
        User user2 = (User)user1.clone();
        user2.setName("李四");
        user2.setAge(22);
        //修改引用对象的值,不会修改到user1的引用对象的值
        user2.getPerson().setPerson("afternoon!");

        System.out.println("user1:"+user1);
        System.out.println("user2:"+user2);

    

Java创建对象有几种方式?

  1. 用new语句创建对象

  2. 使用反射,即使用Class.newInstance()创建对象。

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

  4. 通过反序列化手段,即调用java,io.ObjcetInputStream对象的readObjcet()方法。

说说类实例化的顺序

Java中类实例化顺序:

  1. 静态属性,静态代码块

  2. 普通属性,普通代码块。

  3. 构造方法。

public class LifeCycle {
    // 静态属性
    private static String staticField = getStaticField();

    // 静态代码块
    static {
        System.out.println(staticField);
        System.out.println("静态代码块初始化");
    }

    // 普通属性
    private String field = getField();

    // 普通代码块
    {
        System.out.println(field);
        System.out.println("普通代码块初始化");
    }

    // 构造方法
    public LifeCycle() {
        System.out.println("构造方法初始化");
    }

    // 静态方法
    public static String getStaticField() {
        String statiFiled = "静态属性初始化";
        return statiFiled;
    }

    // 普通方法
    public String getField() {
        String filed = "普通属性初始化";
        return filed;
    }

    public static void main(String[] argc) {
        new LifeCycle();
    }

    /**
     *      静态属性初始化
     *      静态代码块初始化
     *      普通属性初始化
     *      普通代码块初始化
     *      构造方法初始化
     */
}

Java中常见的关键字有哪些?

static

static可以用来修改类的成员方法,类的成员变量。

static变量也称作静态变量。静态变量和非静态变量的区别是:静态变量被所有对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化。存在多个副本,各个对象拥有的副本互不影响。

public class Person {
    String name;
    int age;
    
    public String toString() {
        return "Name:" + name + ", Age:" + age;
    }
    
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "zhangsan";
        p1.age = 10;
        Person p2 = new Person();
        p2.name = "lisi";
        p2.age = 12;
        System.out.println(p1);
        System.out.println(p2);
    }
    /**Output
     * Name:zhangsan, Age:10
     * Name:lisi, Age:12
     *///~
}

如果age设置为静态变量,那么在初始化时,age被设置为10,然后设置为12覆盖掉10,最终输出的都是age为10的值。

statci方法一般称作静态方法。静态方法不依赖于任何对象就可以被访问,通过类名即可调用静态方法。

public class Utils {
    public static void print(String s) {
        System.out.println("hello world: " + s);
    }

    public static void main(String[] args) {
        Utils.print("程序员大彬");
    }
}

静态代码块只会在类加载的时候执行一次。

静态内部类,在静态方法中,比如main()方法,使用非静态内部类依赖于外部类的实例,也就是说得先创建外部类的实例,再用这个实例去创建非静态内部类。而静态内部类不需要。

class OuterClass {
    class InnerClass {
        public InnerClass() {
            System.out.println("非静态内部类成功调用");
        }
    }
    static class StaticInnerClass {
        public StaticInnerClass() {
            System.out.println("静态内部类成功调用");
        }
    }
    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();

        StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
}

final

  1. 基本数据类型用final修饰,则不能修改,是常量,初始化后不能修改。对象引用用fina修饰,则引用只能指向该对象,不能指向别的对象,但是对象本身可以修改。

  2. final修饰的方法不能被子类重写。

  3. final修饰的类不能被继承。

this

this.属性名称 指访问类中的成员变量,可以用这来区分成员和局部变量。

this.方法名称 用来访问本类中的方法

super

super关键字用于在子类中访问父类的变量和方法。

接口与抽象类的区别

接口

接口是 Java 语言中的一个抽象类型,用于定义对象的公共行为,不能实例化。它的创建关键字是 interface,在接口的实现中可以定义方法和常量,其普通方法是不能有具体的代码实现的,而在 JDK 8 之后,接口中可以创建 static 和 default 方法了,并且这两种方法必须有默认的方法实现,如下代码所示:

public interface Interface_1 {
    int count = 1;
    void sayHi();
    // default 方法
    default void print() {
        System.out.println("Do print method.");
    }
    // static 方法
    static void smethod() {
        System.out.println("Do static method.");
    }
}

接下来,创建一个类来实现以上接口:

public class InterfaceImpl_1 implements Interface_1 {
    @Override
    public void sayHi() {
        System.out.println("Hi,I am InterfaceImpl 1.");
    }
    public static void main(String[] args) {
        InterfaceImpl_1 inter = new InterfaceImpl_1();
        inter.sayHi();
        // 调用接口中 static 方法
        InterfaceExample.smethod();
        // 调用接口中的常量 count
        System.out.println(InterfaceExample.count);
    }

结论:

  • 使用接口使用implements关键字。

  • 接口中的变量默认public static final类型

  • 子类可以不重写接口中的static 和 default方法,不重写的情况下,默认调用的是接口的方法实现。

  • 接口只可以继承一个或多个其它接口

  • 接口中不能含有静态代码块以及静态方法

抽象类

抽象类和接口类型,也是用来定义对象的公共行为,并且也不能直接实例化

public abstract class AbstractExample {
    // 定义普通变量
    int count = 2;
    // 定义私有变量
    private static int total = 10;
    // 定义抽象方法
    public abstract void methodA();
    // 定义普通方法
    public void methodB() {
        System.out.println("Hi,methodB.");
    }
}

接下来使用一个普通类继承上面的抽象类:

public class AbstractSon extends AbstractExample {
    @Override
    public void methodA() {
        System.out.println("Hi,method A.");
    }
    public static void main(String[] args) {
        AbstractSon abs = new AbstractSon();
        // 抽象类中的变量重新赋值
        abs.count = 666;
        System.out.println(abs.count);
        // 抽象类中的抽象方法
        abs.methodA();
        // 抽象类中的普通方法
        abs.methodB();
    }
}

结论:

  • 抽象类使用abstract关键字声明。

  • 抽象类中可以包含普通方法和抽象方法,普通方法必须有实现,抽象方法不能有具体的代码实现。

  • 抽象类使用extends关键字实现继承。

  • 抽象类中的属性控制符无限制,可以定义private类型的属性。但是抽象方法不能被private修饰

  • 抽象方法可以继承一个类和实现多个接口

  • 抽象类可以有静态代码块和静态方法

  • 除了不能实例化抽象类,抽象类和普通Java类没什么区别。

设计层面上的区别

  • 抽象层次不同。抽象类是对整个类整体进行抽象,包括属性,行为,但是接口只是对类行为进行抽象。继承抽象类是一种"是不是"的关系,而实现接口则是"有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而实现接口则是具不具备的关系,比如鸟是否能飞。

  • 只有相似特点的类才能继承抽象类,比如鸟类,但是实现接口的类却可以不含有相似特点,比如鸟会飞,飞机也会飞。

class AlarmDoor extends Door implements Alarm {
    //code
}

class BMWCar extends Car implements Alarm {
    //code
}

常见的异常有哪些?

为了能够及时有效地处理程序中的运行错误,Java 专门引入了异常类。在 Java 中所有异常类型都是内置类 java.lang.Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层。Throwable 类下有两个异常分支 Exception 和 Error

Throwable 类是所有异常和错误的超类,下面有 Error 和 Exception 两个子类分别表示错误和异常。其中异常类 Exception 又分为运行时异常和非运行时异常,这两种异常有很大的区别,也称为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

  • Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。

  • Error 定义了在通常环境下不希望被程序捕获的异常。一般指的是 JVM 错误,如堆栈溢出。

不讨论关于Error 类型的异常处理,因为它们通常是灾难性的致命错误,不是程序可以控制的。接下来将讨论 Exception 类型的异常处理。


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

非运行时异常是指 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、ClassNotFoundException 等以及用户自定义的 Exception 异常(一般情况下不自定义检查异常)。

  • 下图是Java中常见的运行时异常

异常类型

说明

ArithmeticException

算术错误异常,如以零做除数

ArraylndexOutOfBoundException

数组索引越界

ArrayStoreException

向类型不兼容的数组元素赋值

ClassCastException

类型转换异常

IllegalArgumentException

使用非法实参调用方法

lIIegalStateException

环境或应用程序处于不正确的状态

lIIegalThreadStateException

被请求的操作与当前线程状态不兼容

IndexOutOfBoundsException

某种类型的索引越界

NullPointerException

尝试访问 null 对象成员,空指针异常

NegativeArraySizeException

再负数范围内创建的数组

NumberFormatException

数字转化格式异常,比如字符串到 float 型数字的转换无效

TypeNotPresentException

类型未找到

  • 下图是Java中常见的非运行时异常

异常类型

说明

ClassNotFoundException

没有找到类

IllegalAccessException

访问类被拒绝

InstantiationException

试图创建抽象类或接口的对象

InterruptedException

线程被另一个线程中断

NoSuchFieldException

请求的域不存在

NoSuchMethodException

请求的方法不存在

ReflectiveOperationException

与反射有关的异常的超类

Error和Exception的区别?

Error:JVM无法解决的严重问题,如栈溢出 StackOverflowError 、 内存溢出 OOM 等程序无法处理的错误

Exception:因编程错误或偶然的外在因素导致的一般问题,可以在代码中处理,如:空指针异常,数组下标越界等。

throw和throws的区别

throws告诉调用者可能会出现这些异常,你需要先去处理这些异常,throw是直接就告诉你出异常了。

 void doA(int a) throws Exception1,Exception3{
           try{
                 ......
           }catch(Exception1 e){
              throw e;
           }catch(Exception2 e){
              System.out.println("出错了!");
           }
           if(a!=b)
              throw new  Exception3("自定义异常");
}

代码中可能会产生3个异常,Exception1,Exception2,Exception3,如果产生Exception1异常,则捕获之后再抛出,由该方法的调用者去处理。如果产生Exception2异常,那么由doA方法自己捕获并处理,不需要该方法调用者去处理,所以throws没有说需要处理Exception2异常。没有捕获,而是程序的逻辑出错而抛出Exception3异常,也是需要调用者去处理。

Java支持多继承吗?

Java中,类不支持多继承,接口才支持多继承。接口的作用就是扩展对象功能,当一个子接口继承了多个父接口,说明子接口扩展了多个功能。当一个类实现该接口时,就相当于扩展了多个功能。

Java提供了接口和内部类以达到实现多继承的功能,弥补单继承的缺陷。

如何实现对象的克隆?

  • 实现Cloneable接口,重写clone()方法。分为浅拷贝和深拷贝。

  • 序列化和反序列化,深拷贝。

  • 通过org.apache.commons 中的工具类 BeanUtils 和PropertyUtils 进行对象复制。

简述下Java8 的新特性?

  • Lambda表达式 :Lambda允许把函数作为一个方法的参数。

  • Stream API :支持对元素流进行函数式操作。Stream API 集成在 Collections API 中,可以对集合进行批量操作,例如顺序或并行的 map-reduce 转换。

  • 默认方法:就是在接口可以有默认实现方法

  • Optional类:Optional类已成为Java8类库的一部分,用来解决空指针异常。

  • Date Time API:加强对日期与时间的处理。

如何停止一个正在运行的线程?

  1. 使用线程的stop方法

使用stop()方法可以强制终止线程,不推荐使用。

  1. 使用interrupt方法中断线程

该方法只是告诉线程要终止,但最终何时终止取决于计算机。调用interrupt方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。

接着调用Thread.currentThread().isInterrupt()方法,可以用来判断当前线程是否被终止,通过这个判断我们可以做一些业务逻辑处理,通常如果isInterrupted返回true的话,我们可以抛出一个中断异常InterruptedException ,然后通过try-catch捕获。

class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1000); //保证MyThread线程运行
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end1");
    }
}

class MyThread extends Thread {
    public void run() {
        Thread hello = new HelloThread();
        hello.start(); // 启动hello线程
        try {
            hello.join(); // 等待hello线程结束
        } catch (InterruptedException e) {
            System.out.println("interrupted!");
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
                System.out.println("end2");
                break;
            }
        }
    }
}

main线程通过调用t.inturrupt()从而通知t线程中断线程,而此时t线程正处于hello.join()等待中,此方法会立即停止等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此就可以正常结束该线程,在t线程结束前,对hello线程也进行了interrupt()通知其中断。如果去掉这一行,则hello线程仍会进行。

  1. 设置标志位

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true; //注意必须用volatile修饰
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

什么是泛型?

Java泛型是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数。声明的类型参数在使用时用具体的类型来替换。

泛型的最大好处是可以提高代码的复用性。以List接口为例,我们可以将String,Integer等类型放入List中,如不用泛型,存放String要写一个List接口,存放Integer又要写一个List接口,泛型就可以解决这个问题。

Java序列化和反序列化三连问:是什么?为什么要?如何做?

Java序列化和反序列化是什么?

Java序列化是指把Java对象转化为字节序列的过程,而Java反序列化是指将字节序列恢复为Java对象的过程。

  • 序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地中。核心作用是对象状态的保存和重建。

  • 反序列化:客户端从文件中或者网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态以及描述信息,通过反序列化重建对象。

为什么需要序列化和反序列化?

序列化的好处:

  1. 对象序列化可以实现分布式对象。

主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象一样。Java的RMI远程调用是指,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。

  1. Java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。

可以将整个对象层次写入字节流中,可以保存在文件中或者在网络连接上传递。利用对象序列化可以进行对象的“深复制”,即复制对象本身及引用的对象本身。序列化一个对象可能得到全部对象序列。

  1. 序列化可以将内存中的类写入文件或者数据库中。

比如:将某个类序列化后存为文件,下次读取时只需要将文件中的数据反序列化就可以将原先的类还原到内存中,或者序列化为数据流传输。

  1. 对象,文件,数据,有很多不同的格式,很难统一传输和保存。

无论原来是什么东西, 序列化以后都是字节流,字节流可以用通用的格式传输或者保存,传输结束后,要再次使用,就反序列还原成原来对象或者文件。

如何序列化和反序列化?

import java.io.Serializable;
public class Person implements Serializable { //本类可以序列化

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return "姓名:" + this.name + ",年龄" + this.age;
    }
}
package org.lxh.SerDemo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class ObjectOutputStreamDemo { //序列化
    public static void main(String[] args) throws Exception {
        //序列化后生成指定文件路径
        File file = new File("D:" + File.separator + "person.ser");
        ObjectOutputStream oos = null;
        //装饰流(流)
        oos = new ObjectOutputStream(new FileOutputStream(file));
        
        //实例化类
        Person per = new Person("张三", 30);
        oos.writeObject(per); //把类对象序列化
        oos.close();
    }
}

反序列化对象时,需要创建一个 ObjectInputStream 输入流,然后调用 ObjectInputStream 对象的 readObject() 方法得到序列化的对象即可。

public class SerializableTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("abc.txt"));
        Person p = (Person) ois.readObject();
        System.out.println(p);
    }

补充:transient关键字的作用?

transient修饰变量,当对象被序列化时,被该关键字修饰的变量的值会被忽略,反序列化时该变量设置为初始值,比如int型时0,对象型是null。

什么是反射?

动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

在运行状态中,对于任意一个类,能够知道这个类的所有属性和方法,对于任意一个对象,能够调用它的任意方法和属性。

反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。

一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);

上面这样子进行类对象的初始化,我们可以理解为「正」。

而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。

这时候,我们使用 JDK 提供的反射 API 进行反射调用:

public class Apple {

    private int price;

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public static void main(String[] args) throws Exception{
        //正常的调用
        Apple apple = new Apple();
        apple.setPrice(5);
        System.out.println("Apple Price:" + apple.getPrice());
        //使用反射调用
        Class clz = Class.forName("com.chenshuyi.api.Apple");
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        Constructor appleConstructor = clz.getConstructor(); //使用默认的构造方法,注意覆盖了会出错
        Object appleObj = appleConstructor.newInstance();
        setPriceMethod.invoke(appleObj, 14);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
    }
}

可以看出,一般情况下我们使用反射获取一个对象的步骤:

Class clz=Class.forName("com.wzc.people");
Constructor appleConstructor=clz.getConttructor();
Object appleObj = appleConstructor.newInstance();

如果要调用某个方法

Method setPriceMethod = clz.getMethod("setPrice", int.class);
setPriceMethod.invoke(appleObj, 14);

反射常用API

获取反射中的Class对象

在反射中,要获取一个类或调用一个类的方法,首先获取该类的Class对象。

在Java API中, 获取Class类对象有三种方式:

  1. 使用Class.forName静态方法。前提是得知道类的全路径

Class clz = Class.forName("java.lang.String");

  1. 使用.class方法。这种方式只适合在编译前就知道操作的Class

Class clz = String.class;

  1. 使用 类对象 的getClass()方法

String str = newString("Hello");

Class clz = str.getClass();

通过反射创建类对象

通过Class对象的newInstance()方法创建类对象,通过Constructor对象的newInstance()方法创建类对象。

Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();

不过通过Constructor对象创建类对象可以选择特点的构造方法。而通过Class对象创建则只能使用默认的无参构造方法。如下:

Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Apple apple = (Apple)constructor.newInstance("红富士", 15);

通过反射获取类属性、方法、构造器

通过Class对象的getFields()方法可以获取Class类的属性,但无法获取私有属性

Class clz = Apple.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性。与获取类属性一样,当我们去获取类方法、类构造器时,如果要获取私有方法或私有构造器,则必须使用有 declared 关键字的方法。

反射有哪些应用场景?

  1. JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序。

  2. Eclispe,IDEA等开发工具利用反射动态解析对象的类型与结构,动态提示对象属性和方法。

  3. Web服务器中利用反射调用Sevlet的service方法

  4. JDK动态代理底层依赖反射实现。

  5. Spring通过XML配置模式装载Bean的过程。

  • 将程序内所有XML或Properties配置文件加载入内存中

  • Java类在内存里面解析内容。得到对应实体类的字节码字符串以及相关属性的信息

  • 使用反射机制,根据这个字符串获取某个类的Class实例

  • 动态配置实例的属性

这样做的好处是:

  • 不用每一次都要在代码里面去new或者做其它事情

  • 以后要改的话直接改配置文件,代码维护起来就很方便了

  • 有时候为了适应 某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射实现。

介绍一下访问修饰符

Java除了提供的三个访问修饰符分别代表三个访问级别之外还有一个不加修饰符的访问级别,它们访问级别控制从小到大为: private->default->protected->public 他们访问级别分别如下:

private:类中被private修饰的成员只能在当前类的内部被访问。根据这点,我们可以使用它来修饰成员变量,从而将成员变量隐藏在这个类的内部。

default:如果类中的成员或者一个外部类不使用任何访问修饰符来进行修饰,那么他就是default级别的,default访问控制的类成员或者外部类可以被相同包下的其他类访问。

protected:如果一个类成员被protected访问修饰符修饰,那么这个成员不但可以被同一个包下的其他类访问,还可以被其他包下的子类访问。一般来讲,如果一个方法被protected修饰,那么通常是希望它的子类来重写它。

public:这是Java中最宽松的访问级别,如果类成员被这个修饰符修饰,那么无论访问类和被访问类在不在一个包下,有没有父子关系,这个类成员都可以被访问到。

类加载双亲委派模型

JDK自带有三个类加载器: bootstrap ClassLoader, ExtClassLoader, AppClassLoader。

BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%/lib下的jar包和class文件。

ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。

AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。

JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先使用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会自己尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。

所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载。

一个对象从加载到JVM,再到GC清除,都经历了什么过程?

  1. 首先把字节码文件内容加载到方法区

  2. 然后再根据类信息(方法区)在堆区创建对象

  3. 对象首先会分配在堆区中年轻代的Eden区,经过一次Minor GC后,对象如果存活,就会进入Suvivor区。在后续的每次Minor GC中,如果对象一直存活,就会在Suvivor区来回拷贝,每移动一次,年龄加1

  4. 当年龄超过15后,对象依然存活,对象就会进入老年代,

  5. 如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清理掉

怎么确定一个对象是垃圾

  1. 引用计数算法:这种方式是给堆内存当中的每个对象记录一个引用个数。引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。

  2. 可达性算法:这种方式是在内存中,从根对象向下一直找引用,找到的对象就不是垃圾,没找到的对象就是垃圾。

JVM有哪些垃圾清回收算法

  1. 标记清除算法:

a.标记阶段:把垃圾内存标记出来

b.清除阶段:直接将垃圾内存回收。

c.这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。

  1. 复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。

  2. 标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。