JAVA常见问题合集

面向对象

  • 面向过程和面向对象

  • 面向对象的三大基本特征:封装、继承、多态

 - 封装:隐藏内部细节

  • 继承:复用现有代码

  • 多态:改写对象行为

  • JAVA为什么是面向对象的,为什么还用int等基础类型

    • 面向对象的特征:封装,继承,多态。JAVA语言符合这些特征。
    • 因为在每个封装体里,基本还是面向过程的代码思想,因此还是需要这些基础数据类型。
  • 面向对象软件开发的优点有哪些?

    • 更符合人们思考习惯的思想,更多体现的是指挥者,指挥对象做事情,与面向过程的执行者区别;
    • 可以将复杂问题简单化,保证了较高的开发效率,保证了程序的鲁棒性和可维护性。
    • 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统。
    • 代码开发模块化,更易维护和修改。
    • 增强代码的可靠性和灵活性。
    • 增加代码的可理解性。
  • 1、封装

    • 封装:隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别,将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。
    • 封装的目的:是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,以特定的访问权限来使用类的成员。
    • 面向对象就是使用程序处理事情时以对象为中心去分析;与面向过程不同,面向过程关心处理的逻辑、流程等问题,而不关心事件主体。而面向对象即面向主体,所以我们在解决问题时应该先进行对象的封装(对象是封装类的实例,比如张三是人,人是一个封装类,张三只是对象中的一个实例、一个对象)。比如我们日常生活中的小兔子、小绵羊都可以封装为一个类。
  • 封装的定义和好处有哪些?

    • 一是提高了数据的安全性。用private把类的细节与外界隔离起来,从而实现数据项和方法的隐藏,而要访问这些数据项和方法唯一的途径就是通过类本身,类才有资格调用它所拥有的资源(方法,数据项属性等等)。
    • 二是提高了代码的可用性和可维护性。通过隐藏隔离,只允许外部对类做有限的访问,开发者可以自由的改变类的内部实现,而无需修改使用该类的那些程序。只要那些在类外部就能被调用的方法保持其外部特征不变,内部代码就可以自由改变,各取所需,利于分工。
    • 三就是提高了代码的重用性,封装成工具类以后能够减少很多繁琐的步骤。
  • 抽象的定义?抽象和封装的不同点?

    • 抽象是把想法从具体的实例中分离出来的步骤,因此,要根据他们的功能而不是实现细节来创建类。Java支持创建只暴漏接口而不包含方法实现的抽象的类。这种抽象技术的主要目的是把类的行为和实现细节分离开。
    • 抽象和封装是互补的概念。一方面,抽象关注对象的行为。另一方面,封装关注对象行为的细节。一般是通过隐藏对象内部状态信息做到封装,因此,封装可以看成是用来提供抽象的一种策略。
  • 2、继承

    • 继承是指:保持已有类的特性而构造新类的过程。继承后,子类能够利用父类中定义的变量和方法,就像它们属于子类本身一样。
      • 继承是面向对象的基本特征之一,支持单继承,不支持多继承;继承机制允许创建分等级层次的类。
      • 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
      • 继承之间是子父类的关系。继承机制可以很好的描述一个类的生态,也提高了代码复用率,在Java中的Object类是所有类的超类,常称作上帝类。
    • 作用:
      • 继承提高了代码复用性,提供了多态的前提。
  • 单继承和多继承

    • 只支持单继承(一个类只能有一个父类),不支持多继承(多继承用接口实现)。
    • 单继承:java类是单继承的,一个类只允许有一个父类。
      • public class A extends B{ } //继承单个父类
    • 多继承:java接口多继承的,一个类允许继承多个接口。
      • public class A extends B implements C{ } //同时继承父类和接口
      • public class A implements B,C{ } //继承多个接口
    • 支持多重继承
      • class A {}
      • class B extends A{}
      • class C extends B{}
  • 3、多态

    • 某一类事物的多种表现形态。
    • 具体来说,在父类中定义的属性和方法被子类继承之后,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
    • 子类对象的多态性使用前提:
      • 有类的继承或实现关系;
      • 由子类对父类方法的重写(覆盖)
    • 多态:同一个行为具有多个不同表现形式或形态的能力。
      • 是指一个类实例(对象)的相同方法在不同情形有不同表现形式。
      • 多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
    • 多态的优点:
      • 消除类型之间的耦合关系
      • 可替换性
      • 可扩充性
      • 接口性
      • 灵活性
      • 简化性
    • 多态存在的三个必要条件:
      • 继承
      • 重写(子类继承父类后对父类方法进行重新定义)
      • 父类引用指向子类对象
    • 多态的成员变量和成员函数:
      • 成员变量和静态变量:编译和运行都看左边(父类)
      • 成员函数:编译看左边(父类),运行看右边(子类)
    • 写一个多态:
class Fu{
	int a = 3;
	void show() {
		System.out.println("fu show.");
	}
}

class Zi extends Fu{
	int a = 5;
	void show() {
		System.out.println("zi show.");
	}
}

public class DuotaiDemo {
	public static void main(String[] args) {
		Fu fu = new Fu();
		fu.show();
		
		Fu f = new Zi();
		System.out.println("f.a = "+f.a);
		f.show();
		
		Zi z = new Zi();
		System.out.println("z.a = "+z.a);
		z.show();
	}
}

//结果
fu show.
f.a = 3
zi show.
z.a = 5
zi show.

java基础

  • Integer和int的区别

    • int则是java的一种基本数据类型,Integer是int的包装类
    • int的默认值是0,Integer的默认值是null
    • Integer变量必须实例化后才能使用,而int变量不需要
    • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
  • java常见的几种运行时异常RuntimeException

    • NullPointerException - 空指针引用异常
    • ClassCastException - 类型强制转换异常。
    • IllegalArgumentException - 传递非法参数异常。
    • ArithmeticException - 算术运算异常
    • ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
    • IndexOutOfBoundsException - 下标越界异常
    • NegativeArraySizeException - 创建一个大小为负数的数组错误异常
    • NumberFormatException - 数字格式异常
    • SecurityException - 安全异常
    • UnsupportedOperationException - 不支持的操作异常
  • Java 异常中的getMessage()和toString()方法的异同

    • e.toString(): 获得异常种类和错误信息
    • e.getMessage():获得错误信息
    • e.printStackTrace():在控制台打印出异常种类,错误信息和出错位置等
  • java中的length和length(),size

  • 重写与重载

    • 重载:
      • 发生在同一个类里面,两个或者是多个方法的方法名相同但是参数不同的情况。
      • 注:参数顺序不同也参数列表不同的情况。
      • 重载与返回值类型和修饰符无关。
    • 重写或覆盖:
      • 是指子类重新定义了父类的方法;
      • 重写必须有相同的方法名,参数列表和返回类型。
      • 子类函数的访问修饰权限不能少于父类的。
  • 栈和堆的区别

    • 1.栈内存存储的是局部变量,基本类型的变量表示的是数据本身;而堆内存存储的是实体,每个实体对象都有地址值和默认初始化值;
    • 2.栈内存的读取和更新速度要快于堆内存,因为局部变量的生命周期很短;
    • 3.栈内存使用一级缓存,存放的变量生命周期一旦结束就会被释放;而堆内存使用二级缓存,存放的实体会被垃圾回收机制不定时的回收。
  • == 和equals

    • ==:
      • 基本数据类型比较的是值
      • 引用类型比较的是地址值
    • equals(Object o):
      • 不能比较基本数据类型,基本数据类型不是类类型
      • 比较引用类型时(该方法继承自Object,在object中比较的是地址值)等同于”==”;
      • 如果自己所写的类中已经重写了equals()方法,那么就按照用户自定义的方式来比较两个对象是否相等,如果没有重写过equals()方法,那么会调用父类(Object)中的equals()方法进行比较,也就是比较地址值。
      • 注意:equals(Object o)方法只能是一个对象来调用,然后参数也应传一个对象。
public boolean equals (Object x){
    return this == x;
}
  • 什么时候用==,什么时候用equals()呢?

    • 如果是基本数据类型:用==比较
    • 如果是引用类型:
      • 想按照自己的方式去比较,就要重写这个类中的equals()方法;如果没有重写,那么equals()和==比较的效果是一样的,都是比较引用的地址值
    • 如果是比较字符串:
      • 直接用equals就可以了,因为String类里面已经重写了equals()方法,比较的是字符串的内容,而不是引用的地址值了。

=============================

  • java容器

    • List、Set、Map、Queue

  • List接口

    • List是有序的 collection(也称为序列)。用户插入的顺序或者指定的位置就是元素插入的位置。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
    • 与Set不同,List允许插入重复的值。
    • List的子类:Vector、ArrayList、LinkedList
      • 1.1) ArrayList (类)
        • ArrayLis是基于数组实现的List类,它封装了一个动态的、增长的、允许再分配的Object[ ]数组.它允许对元素进行快速随机访问
          当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
      • 1.2)Vector (类)
        • Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。所以现在已经不太常用了。
        • 1.2.1)Stack (类):Stack是Vector提供的一个子类,用于模拟"栈"这种数据结构(LIFO后进先出)
      • 1.3)LinkedList (类)
        • LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,它还实现了Deque接口,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
  • Set接口

    • Set是无序、不可重复的。同时,如果有多个null,则不满足单一性了,所以Set只能有一个null。
    • Set判断两个对象相同不是使用"=="运算符,而是根据equals方法。
    • Set的子类:Set最流行的实现类有HashSet、TreeSet、LinkedHashSet(从HashSet继承而来)。
      • 1.1) HashSet (类)
        • HashSet是Set接口的典型实现,HashSet使用HASH算法来存储集合中的元素,因此具有良好的存取和查找性能。
        • HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法的返回值相等。
        • 1.1.1)LinkedHashSet(类)
          • LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但和HashSet不同的是,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。
          • LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
          • LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历)
      • 1.2) SortedSet (接口):
        • 主要用于排序操作,实现了此接口的子类都属于排序的子类
        • 1.2.1)TreeSet(类):SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态
      • 1.3) EnumSet (类)
        • EnumSet是一个专门为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式、或隐式地指定。EnumSet的集合元素也是有序的,
  • Queue 接口

    • 此接口用于模拟“队列”数据结构(FIFO)。新插入的元素放在队尾,队头存放着保存时间最长的元素。
    • Queue的子类、子接口
      • 1.1) PriorityQueue—— 优先队列(类)
        • 其实它并没有按照插入的顺序来存放元素,而是按照队列中某个属性的大小来排列的。故而叫优先队列。
      • 1.2) Deque——双端队列(接口)
        • ArrayDeque(类):基于数组的双端队列,类似于ArrayList有一个Object[] 数组。
        • LinkedList (类)(上文已有,略)

  • Map 接口

    • Map不是collection的子接口或者实现类。Map是一个接口。
    • Map用于保存具有“映射关系”的数据。每个Entry都持有键-值两个对象。其中,Value可能重复,但是Key不允许重复(和Set类似)。
    • Map可以有多个Value为null,但是只能有一个Key为null。
    • Map的子类、子接口
      • 1) HashMap (类)
        • 和HashSet集合不能保证元素的顺序一样,HashMap也不能保证key-value对的顺序。
        • 并且类似于HashSet判断两个key是否相等的标准一样: 两个key通过equals()方法比较返回true、 同时两个key的hashCode值也必须相等
      • 1.1) LinkedHashMap (类)
        • LinkedHashMap也使用双向链表来维护key-value对的次序,该链表负责维护Map的迭代顺序,与key-value对的插入顺序一致(注意和TreeMap对所有的key-value进行排序区分)。
      • 2) HashTable (类):是一个古老的Map实现类。
        • 2.1) Properties(类)
          • Properties对象在处理属性文件时特别方便(windows平台的.ini文件)。Properties类可以把Map对象和属性文件关联,从而把Map对象的key - value对写入到属性文件中,也可把属性文件中的“属性名-属性值”加载进Map对象中。
      • 3) SortedMap(接口)
        • 如同Set->SortedSet->TreeSet一样,Map也有Map->SortedMap->TreeMap的继承关系。
      • 3.1) TreeMap(类)
        • TreeMap是一个红黑树结构,每个键值对都作为红黑树的一个节点。TreeMap存储键值对时,需要根据key对节点进行排序,TreeMap可以保证所有的key-value对处于有序状态。 同时,TreeMap也有两种排序方式:自然排序、定制排序(类似于上面List的重写CompareTo()方法)。
  • 数组与链表

    • 数据中数组的内存是顺序存储的,而链表是随机存取的。
    • 数组随机访问效率很高,但插入删除操作的效率比较低。
    • 链表在插入删除操作上相对数组有很高的效率,而如果访问链表中的某个元素,那就要从表头逐个遍历,直到找到所需要的元素为止,所以链表的随机访问效率比数组低。
    • 链表不存在越界问题,数组有越界问题。
    • 数组节省空间但是长度固定。链表虽然变长,但是占了更多的存储空间。
    • 静态)数组从栈中分配内存空间,对于程序员方便快速,但是自由度小。链表从堆中分配空间,自由度大,但申请管理比较麻烦。
  • arraylist和linkedlist的区别

    • ArrayList和LinkedList都实现了List接口,他们有以下的不同点:
    • ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
    • 相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
    • LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
  • HashMap和Hashtable的区别

    • 底层都是数组+链表实现
    • 1、继承的父类不同: Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
    • 2、线程安全性不同: HashMap是线程不安全的,可用于单线程;Hashtable是线程安全的,可用于多线程。
    • 3、是否提供contains方法
      • HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。
      • Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
    • 4、key和value是否允许null值:其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。
      • Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。
      • HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
    • 5、两个遍历方式的内部实现上不同:
      • Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
    • 6、hash值不同:
      • 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
    • 7、内部实现使用的数组初始化和扩容方式不同:
      • HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
      • Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
  • HashMap为什么是线程不安全的?

    • HashMap是线程不安全的,我们应该使用ConcurrentHashMap
    • Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的
    • HashMap的线程不安全主要体现在下面两个方面:
      • 1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
      • 2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
    • 注:ConcurrentHashMap使用了分段锁技术来提高了并发度,不在同一段的数据互相不影响,多个线程对多个不同的段的操作是不会相互影响的。每个段使用一把锁。所以在需要线程安全的业务场景下,推荐使用ConcurrentHashMap,而HashTable不建议在新的代码中使用,如果需要线程安全,则使用ConcurrentHashMap,否则使用HashMap就足够了。
  • 哈希表:哈希函数构造;哈希表解决地址冲突的方法

    • 散列函数构造方法:
      • 1.直接定址法:H(key) = a*key + b
      • 2.除留余数法:H(key) = key % p(p为不大于散列表表长,但最接近或等于表长的质数p)
      • 3.数字分析法:选取r进制数数码分布较为均匀的若干位作为散列地址
      • 4.平方取中法:取关键字的平方值的中间几位作为散列地址
      • 5.折叠法:将关键字分割成位数相同的几部分,然后取这几部份的叠加和作为散列地址
    • 处理冲突的方法:
      • 1.开放定址法(闭哈希表):在冲突的哈希地址的基础上进行处理,得到新的地址值。Hi = (H(key)+di) % m(m表示散列表表长,di为增量序列)
        • 1)线性探测法:dii=1,2,3,…,m-1
        • 2)二次探测法:di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
          • 冲突发生时,以原哈希地址为中心,在表的左右进行跳跃式探测,比较灵活。
        • 3)伪随机数法:di=伪随机数序列。
          • 具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。
      • 线性探测再散列的优点是:只要哈希表不满,就一定能找到一个不冲突的哈希地址,而二次探测再散列和伪随机探测再散列则不一定。
      • 注:在开放定址的情形下,不能随便物理删除表中已有元素,若删除元素将会截断其他具有相同散列地址的元素的查找地址。若想删除一个元素,给它做一个删除标记,进行逻辑删除。
      • 2.链地址法、拉链法(开哈希表)
        • 将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
      • 3.再哈希法:同时构造多个不同的哈希函数,发生冲突时,使用其他哈希函数求值。这种方法不易产生聚集,但增加了计算时间。
      • 4.建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

=============================

  • String中两种初始化方式比较:直接赋值和构造函数初始化

    • 通过直接赋值创建对象是在方法区的常量池:
      • String s1 = "abc";
      • 首先在常量池中查找"abc",如果没有则在常量池创建该对象
      • 在栈中创建s1的引用,将s1直接指向对象"abc"
      • 因此在这里"abc"是常量池中的对象,如果声明另一个String类型的对象引用,并将它指向对象"abc",则这两个引用指向的是同一个常量池中的对象。
      • 直接赋值:存储在常量池。
      • 多次直接赋值:将存储在常量池中的"hello"的地址传递给s变量,且常量池已有字符串公用。
    • 通过构造方法创建字符串对象是在堆内存:
      • String s = new String(“abc”);
      • 凡是经过 new 创建出来的对象,都会在堆内存中分配新的空间,创建新的对象,所以s是String类新创建的对象;
      • 每new一次,都会创建新的对象,即使对象值相同。
  • String、StringBuilder与StringBuffer

    • 字符修改上的区别
      • String:字符串常量,不可变字符串,每次对String的操作都可能生成新的String对象,效率低下,而且大量浪费有限的内存空间。
        • 注:对string重新赋值,如果字符串常量池不存在这个新的赋值对象,就会创造新的对象,如果存在,就不会创建。
      • StringBuffer:字符串变量,可变字符串、效率低、线程安全;
      • StringBuilder:字符串变量,可变字符序列、效率高、线程不安全;
        • 注:StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
    • 三者在执行速度方面的比较:
      • StringBuilder > StringBuffer > String
    • 继承结构不同
      • String继承自CharSequence接口,StringBuilder和StringBuffer继承自Appendable接口。
    • 小结:
      • (1)如果要操作少量的数据用 String;
      • (2)多线程操作字符串缓冲区下操作大量数据 StringBuffer;
      • (3)单线程操作字符串缓冲区下操作大量数据 StringBuilder(推荐使用)。
  • 辨析:replace,replaceAll,replaceFirst

    • replace和replaceAll:
      • 相同点:替换所有匹配的字符串(都是替换所有)
      • 不同点:replace支持字符替换,字符串替换; replaceAll是正则表达式替换
    • replaceFirst: 同replaceAll一样,也是基于规则表达式的替换
      • 不同之处是:只替换第一次出现的字符串
  • public、protected、缺省、private

    • public修饰的成员变量和函数可以被类、子类、同一个包中的类以及任意其他类访问。
    • protected修饰的成员变量和函数能被类本身、子类及同一个包中的类访问。
    • 缺省情况(不写)下,属于一种包访问,即能被类本身以及同一个包中的类访问。
    • private修饰的成员变量和函数只能在类本身和内部类中被访问。
  • 判断一个类是否“无用”,则需同时满足三个条件:

    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  • 垃圾回收算法:复制算法、标记-清除算法、标记-整理算法、分代收集算法

    • 如何确定某个对象是垃圾:引用计数法。
      • 在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。
      • 这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。
    • 垃圾回收算法:复制算法、标记-清除算法、标记-整理算法、分代收集算法
      • 标记-清除算法:
        • 分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
        • 实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
      • 复制算法:
        • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
        • 实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
        • Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
      • 标记-整理算法:
        • 该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
      • 分代收集算法:
        • 是目前大部分JVM的垃圾收集器采用的算法。
        • 它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
        • 新生代:Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
        • 老年代:Mark-Compact算法。根据老年代的特点:每次回收都只回收少量对象。
        • 在堆区之外还有一个代就是永久代(Permanet Generation):存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
    • 典型的垃圾收集器:
      • 垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。
      • 下面介绍一下HotSpot(JDK 7)虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。
      • 小结:Serial/Serial Old,ParNew,Parallel Scavenge,Parallel Old,CMS,G1
  • 内存溢出和内存泄露

    • 内存泄露:
      • 是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。
      • Java中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,当我们new了一个对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这便会造成内存泄露。
    • 内存溢出:
      • 是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
  • java会不会内存泄漏

    • 内存泄漏:一个不再被程序使用的对象或变量还在内存中占用存储空间。
    • Java的垃圾回收机制可以回收这类不再使用的对象。
    • 但是Java还存在内存泄漏的问题。
    • 原因:
      • 静态集合类,如哈希表:因为是静态的,生命周期与程序一致,在程序结束前不能释放,造成内存泄漏;
      • 变量不合理的作用域:如果一个变量定义的作用范围大于使用范围,可能造成内存泄漏。
      • 其他:建立各种链接后,监听器,单例模式中静态存储单例对象等等。
  • 抽象类

    • 抽象类和抽象方法必须用abstract修饰;
    • 抽象方法:只有方法声明,没有方法体,定义在抽象类中;
    • 格式:修饰符 abstract 返回值类型 函数名(参数列表){}
    • 抽象类不可以被实例化,即不可以用new创建对象。
      • 抽象类通过其子类实例化,而子类需要覆盖掉抽象类中所有抽象方法才可以创建对象,否则该子类也是抽象类。
  • 接口

    • 抽象类和接口:
      • 抽象类中可以定义抽象方法和非抽象方法;
      • 当抽象类中的方法都是抽象的时,可以定义为接口。
    • 接口最重要的体现:解决单继承的弊端。
      • 多继承父类中有方法主体,导致调用时的不确定性;
      • 接口中没有方法体,由子类来定义。
    • 接口的特点:
      • 接口不可以创建对象;
      • 子类必须覆盖掉接口中的所有的抽象方法后,子类才可以实例化,否则子类是一个抽象类。
    • 固定修饰符:
      • 成员变量(其实是常量):public static final
      • 成员方法:public abstract
    • 代码体现:
interface A{ void show();}
interface B{ void show();}
class C implements A,B{public void show(){...}}
C c = new C();
c.show();
  • 抽象类和接口的区别

    • 共性:都是不断抽取出来的抽象的概念。
    • 不同1:
      • 抽象类体现的是继承关系,一个类只能单继承;
      • 接口体现的是实现关系,一个类可以多实现。
    • 不同2:
      • 抽象类是继承,是is a的关系;
      • 接口是实现,是like a的关系。
    • 不同3:
      • 抽象类中可以定义非抽象方法,供子类直接使用;
      • 接口的方法都是抽象,接口中的成员都有固定修饰符。
  • 多线程的实现方式有哪些 extend Thread、implement runnable、implement callable

    • 继承Thread类。
      • Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。让自己的类直接extends Thread,并在此类中复写run()方法。
      • 启动线程的方法就是通过Thread类的start()实例方法,start()方法将启动一个新线程,并执行其中的run()方法。
      • 代码实现:
public class MyThread extends Thread {  //继承Thread类
  public void run() {  //复写run()方法
   System.out.println("MyThread.run()");  
  }  
}  

MyThread myThread1 = new MyThread();  //创建一个myThread实例
MyThread myThread2 = new MyThread();  
myThread1.start();  //启动线程
myThread2.start();  

  • 实现Runnable接口。
    • 如果自己的类已经extends另一个类了,就无法再直接extends Thread,此时,可以通过让它来实现Runnable接口来创建多线程。
    • 代码实现:
public class MyThread extends OtherClass implements Runnable {  //实现Runnable接口
  public void run() {  //复写run()方法
   System.out.println("MyThread.run()");  
  }  
}  

MyThread myThread = new MyThread();  //创建一个myThread实例
Thread thread = new Thread(myThread);  //将自己的myThread传入Thread实例中
thread.start();  //启动线程
  • 实现Callable接口,重写call函数。

    • 继承Thread类实现多线程,但重写run()方法时没有返回值也不能抛出异常,使用Callable接口就可以解决这个问题。
  • Callable接口和Runnable接口的不同之处:

    • Callable规定的方法是call(),而Runnable是run();
    • call()方法可以抛出异常,但是run()方法不行;
    • Callable对象执行后可以有返回值,运行Callable任务可以得到一个Future对象,通过Future对象可以了解任务执行情况,可以取消任务的执行,而Runnable不可有返回值。
  • 两个线程交替打印hello

//只写关键的同步部分
public class Test {
   public static void main(String[] args) {
       final PrintAB print = new PrintAB();

       new Thread(new Runnable() {
           public void run(){
               for(int i=0;i<5;i++) {
                   print.printA();
                   }
               }
       }).start();

       new Thread(new Runnable() {
           public void run() {
               for(int i=0;i<5;i++) {
                   print.printB(); }
                            }
         }).start();
       }
 }

 class PrintAB{
    private boolean flag = true;

    public synchronized void printA () {
            while(!flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                             } }
                System.out.print("你好+A");
                 flag = false;
                 this.notify();
            }

    public synchronized void printB () {
            while(flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print("你好+B");
            flag = true;
            this.notify(); }
 }
  • 序列化和反序列化

    • 定义:
      • Java序列化就是指把Java对象转换为字节序列的过程。
      • Java反序列化就是指把字节序列恢复为Java对象的过程。
    • 作用:
      • 序列化:在传递和保存对象时,保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
      • 反序列化:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
    • 总结:核心作用就是对象状态的保存和重建。

设计模式

  • 单例模式

    • 什么情况下会用到:
      • 假如有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig对象的实例,这就导致系统中存在多个AppConfig的实例对象,在配置文件内容很多的情况下会严重浪费内存资源。类似AppConfig这样的类,我们希望在程序运行期间只存在一个实例对象。
    • 优点:速度快、在使用时不需要创建、直接使用即可。
    • 缺点:可能存在内存浪费
  • 单例模式代码:

//单例模式
//饿汉式:直接new对象,开发多见
public class Test1 
{
	//私有构造函数
	private Test1(){}
	//自行创建私有、静态的对象
	private static Test1 uniqueInstance1 = new Test1();
	//对外提供公共、静态的访问接口
	public static Test1 getInstance(){
		return uniqueInstance1;
}

//懒汉式
public class Test2
{
	//私有构造函数
	private Test2(){}
	//自行创建私有、静态的空对象
	private static Test2 uniqueInstance2 = null;
	//对外提供公共、静态的访问接口,并创建对象
	public static Test2 getInstance()
	{
		if(uniqueInstance2==null)
			uniqueInstance2 = new Test2();
		return uniqueInstance2;
	}
}

//懒汉式之并发访问
class Single
{
	private Single(){}
	private static Single s = null;
	/*
	并发访问有安全隐患,所以加入同步机制解决安全问题
	但是,同步的出现却降低了效率。
	提高效率:减少判断锁的次数,可以通过双重判断的方式。
	*/
	public static void getInstance()
	{
		if(s==null)
		{
			synchronized(Single.class){
				if(s==null)
					s = new Single();
				return s;
			}
		}
	}
}

END

posted @ 2022-04-19 21:18  hanease  阅读(669)  评论(0编辑  收藏  举报