java学习——java基础(1)

一点关于java基础概念的学习笔记,如果有不对的地方欢迎指正,谢谢!

面向对象的三大特性

封装:封装是面向对象的核心思想,它涉及将对象的属性和行为封装在一个不可分的系统单位中,尽可能隐藏对象的内部细节。这种封装使得对象具有独立性,仅通过受保护的接口与外界交互,从而提高了数据的安全性和隐私性。
继承:继承描述了类与类之间的关系,允许在无需重新编写原有类的情况下,对原有类的功能进行扩展。通过继承,一个新类(派生类)可以从现有的类(基类或父类)中派生,继承基类的属性和方法,并可以添加新的方法和属性以适应特殊需求。
多态:多态基于继承,它允许不同类的对象对同一消息作出响应,表现出不同的行为。多态性使得同一个方法在不同的类中可以有不同的实现,从而提高了程序的灵活性和代码重用性。

Java的基本数据类型

Java的基本数据类型共有8种,分别为byte(字节型,占用1个字节)、short(短整型,占用2个字节)、int(整型,占用4个字节)、long(长整型,占用8个字节)、float(单精度浮点型,占用4个字节)、double(双精度浮点型,占用8个字节)、char(字符型,占用2个字节)和boolean(布尔型,只有true和false两个值)。

Object类下的方法有哪些?

clone(): 创建并返回此对象的一个副本。需要注意的是,这是一个受保护的方法,只有实现了Cloneable接口的对象才可以调用此方法,否则会抛出CloneNotSupportedException异常。
getClass(): 返回对象运行时的类。这是一个final方法,用于获取对象的实际类型。
toString(): 返回对象的字符串表示。这个方法通常被子类重写以提供更有意义的字符串表示。
finalize(): 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类可以重写此方法以清理资源,但由于无法确定该方法何时被调用,因此很少使用。
equals(Object obj): 指示其他某个对象是否与此对象“相等”。子类通常需要重写此方法以提供特定的相等性逻辑。
hashCode(): 返回对象的哈希码值。如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象的hashCode方法必须产生相同的整数结果。子类在重写equals方法时通常也需要重写hashCode方法。
wait(), wait(long timeout), wait(long timeout, int nanos): 使当前线程等待直到另一个线程调用该对象的notify()方法或notifyAll()方法,或者经过指定的时间量。这些方法主要用于线程间通信和同步。
notify(): 唤醒正在等待对象监视器的单个线程。
notifyAll(): 唤醒正在等待对象监视器的所有线程。

JDK、JRE、JVM三者之间的关系

JDK(java development kit):java开发工具包,用来开发java程序的,针对java开发者。
JRE(java runtime environment):java运行时环境,针对java用户。
JVM(java virtual machine):java虚拟机用来解释执行字节码文件(class文件)的。
JDK包含JRE,JRE包含JVM。

在这里插入图片描述

final关键字的作用

修饰类时,该类不可被继承;
修饰方法时,该方法不能被子类重写;
修饰变量时,该变量⼀旦被赋值就不可以更改它的值,只能赋值一次,即为常量。

Java 8之后的Interface接口里能写方法体吗?为什么?

从Java 8开始,interface接口中引入了默认方法和静态方法的概念,这两种方法都允许我们在接口中编写具体的方法体。
在Java 8中,我们可以在接口中定义带有方法体的默认方法。这些方法使用default关键字进行标记。默认方法的主要目的是允许开发者向接口添加新方法,而不会破坏已经实现该接口的现有类的实现。 这样,当我们需要在接口中添加新方法时,不需要修改所有实现了该接口的类。这些类会自动继承默认方法的实现,除非它们选择覆盖这个方法。
除了默认方法外,Java 8还允许在接口中定义静态方法。静态方法也包含具体的方法体,但它们只能通过接口名直接调用,不能通过接口的实例调用。静态方法通常用于提供与接口相关的工具或辅助功能。

==和equals方法之间的区别

==:对于基础数据类型,比较的是他们的值是否相等,比如两个int类型的变量,比较的是变量的值是否一样。对于引用数据类型,比较的是引用的地址是否相同,比如说新建了两个User对象,比较的是两个User的地址是否一样。

equals:用来检测两个对象是否相等,即两个对象的内容是否相等,没有 == 运行速度快。(补充:只有重写后的equals方法才是比较是否相等,Object类里原生的equals方法底层逻辑就是用的 == 实现判断相等)

int类型和Integer类型的比较大小应该要注意什么?

在java中,int是Java中的一种基本数据类型,属于值类型,可以直接比较。而Integer则是Java中的一种包装类型,属于引用类型,不能直接比较。
一个 Integer 对象与一个 int 值进行比较时,Java会自动进行拆箱操作,即将 Integer 对象转换为 int 基本数据类型,然后再进行比较。反之,当需要将一个 int 值赋值给 Integer 对象时,Java会自动进行装箱操作

由于Java中对整型常量池的规定,对于-128~127之间的整数,无论我们创建多少个Integer对象,它们所引用的对象在内存中地址都是相同的,这是因为Java对这个范围内的 Integer 对象进行了缓存(IntegerCache),因此使用 == 进行比较时会返回true。但是对于超过这个范围的整数,每创建一个Integer对象,就会在内存中新分配一个对象,因此 == 进行比较时会返回false。所以在比较Integer对象时,最好使用equals()方法进行比较。

由于 Integer 是一个对象类型,它可以为 null。在与 int 进行比较之前,还应该检查 Integer 对象是否为 null,否则会出现空指针异常。

String、StringBuffer、StringBuilder的区别

String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁,如果尝试去修改,会新生成⼀个字符串对象。

StringBuffer对象则是可变类,当一个StringBuffer对象被创建以后,可以通过StringBuffer类提供的append()等方法改变这个字符串对象的字符序列,一旦得到了想要的字符串对象,即可通过调用toString()方法得到对应的String对象。

StringBuilder和StringBuffer功能和用法基本相似,但是StringBuffer通过在类方法中添加synchronized关键字(相当于给方法加锁),使StringBuffer保证了线程安全,而StringBuilder则没有实现线程安全功能。

String为什么不属于基本类型?

对于基本类型,它们存储的通常是单一的、固定大小的数据。这些数据在内存中占据固定的空间,并且它们的值可以直接被修改。
String类是不可变的,对String类的任何改变,都是返回一个新的String类对象,前后的地址hash值会发生变化。 String 对象是 System.Char 对象的有序集合用于表示字符串。String 对象的值是该有序集合的内容,并且该值是不可变的。
java 中String 是个对象,是引用类型。
基础类型与引用类型的区别是:

基础类型只表示简单的字符或数字,引用类型可以是任何复杂的数据结构。
java虚拟机处理基础类型与引用类型的方式是不一样的,对于基本类型,java虚拟机会为其分配数据类型实际占用的内存空间,对于引用类型变量,他仅仅是一个指向堆区中某个实例的指针。

String类型为什么不可变?

1.安全性:字符串的不可变性提供了更高的安全性。因为字符串内容无法被修改,所以它们可以在多线程环境中安全地共享,而无需担心数据不一致或竞态条件。这简化了并发编程,并减少了出错的可能性。
2.缓存哈希值:由于字符串是不可变的,它的哈希值只需要计算一次,然后缓存起来供以后使用。这对于在哈希表(如HashMap和HashSet)中使用字符串作为键非常有利,因为哈希表的性能在很大程度上依赖于键的哈希值计算速度。
3.字符串常量池:Java中的字符串字面量会被存储在字符串常量池中,以便复用。如果字符串是可变的,那么这种复用就不可能实现,因为一旦一个字符串被修改,它就不能再代表原来的字面量了。不可变性使得字符串池成为一种有效的优化手段,减少了内存占用和垃圾回收的频率。

字符串字面量具体是什么?

字面量是用于表达源代码中一个固定值的表示法,字符串字面量是指双引号引住的一系列字符,双引号中可以没有字符,可以只有一个字符,也可以有很多个字符。当然这里指的是编译后的class文件中的字符串对象

重载和重写的区别

  1. 重载(overload): 发⽣在同一个类中,方法名相同时,可以同时创建两个参数类型不同、个数不同、顺序不同的同名方法,重载表现编译时的多态性(注意:在只有返回类型不同,方法名,参数类型、个数、顺序都完全相同时,不能够实现方法重载)。
  2. 重写(override): 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类,如果父类方法访问修饰符为private则子类就不能重写该方法。重写多用于实现接口,表现为运行时的多态性。

List和Set的区别

● List(列表):有序,按对象进⼊的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,再逐⼀遍历,还可以使用get(int index)获取指定下标的元素。
● Set(集合):无序,不可重复,最多允许有⼀个Null元素对象,取元素时只能用Iterator接口取得所有元素,在逐⼀遍历各个元素。

遍历List动态删除指定元素有几种方式?

1.for循环遍历List中的每个元素,然后判断是否是匹配条件的元素,然后将其移除

         for (int i = 0; i < list.size(); i++) {
            String str = list.get(i);
            if ("target".equals(str)) {
                list.remove(i);
            }
        }

这种处理方式虽然不会报错,但是可能没办法得到正确的结果。因为每次循环i的值会加一,但是list.size()在不断减少,所以 list 就会早早结束了循环。
2.在for循环前先用一个变量存储初始的list.size(),然后再进行循环,和方法1一样。

        int size = list.size();
        for (int i = 0; i < size; i++) {
            String str = list.get(i);
            if ("target".equals(str)) {
                list.remove(i);
            }
        }

这种方法会报出下标越界异常的错误,因为在执行循环的时候,list的size是在不断减少的,所以循环到后面会list的size已经比最初小了很多,这时候访问不到对应下标,会抛出异常。
3.使用倒序的方式进行for循环。

for (int i = list.size() - 1; i > 0; i--) {
            String str = list.get(i);
            if ("target".equals(str)) {
                list.remove(i);
            }
        }

因为i的初始值是list.size()-1,并且每次循环i都会减一,这是和list.size共同减少,所以可以正常运行。
4.使用增强的for循环删除

        for (String item : list) {
            if ("target".equals(item)) {
                list.remove(item);
            }
        }

这种写法也会出现异常,即并发修改异常ConcurrentModificationException。
这是因为增强的 for循环,其内部是调用的 Iterator 的方法,取下个元素的时候都会去判断要修改的数量和期待修改的数量是否一致,不一致则会报错,而 ArrayList 中的 remove 方法并没有同步期待修改的数量值,所以会抛异常。

5.采用迭代器的方式进行删除。

Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            String str = iterator.next() ;
            if ("target".equals(str)){
                iterator.remove();
            }
        }

因为迭代器中的remove 方法将期待修改的数量值进行了同步,所以可以正常运行。
最优的解决方法是迭代器方法进行删除。

说说迭代器是什么?

迭代器不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayList 和 HashSet 等集合。Iterator 是 Java 迭代器最简单的实现,ListIterator 是 Collection API 中的接口, 它扩展了 Iterator 接口。Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素。
Iterator iter = list.iterator(); // 注意iterator,首字母小写
iterator是为了实现对Java容器进行遍历功能的一个接口。在iterator实现了Iterator接口后,相当于把一个Collection容器的所有对象,做成一个线性表(List),而iterator本身是一个指针,开始时位于第一个元素之前。
迭代器 iterator 的两个基本操作是 next 、hasNext 和 remove。
调用 iterator.next() 会返回迭代器的下一个元素,并且更新迭代器的状态
调用 iterator.hasNext() 用于检测集合中是否还有元素
调用 iterator.remove() 将迭代器返回的元素删除

ArrayList和LinkedList区别

ArrayList和LinkedList本质上都是对List接口的实现,只不过ArrayList底层是基于动态数组(Array)实现,LinkedList底层则是基于链表(Link)实现。

随机查找时,ArrayList的查找效率相对会更高,因为LinkedList还需要移动链表指针从前向后线性查找;在进行插入、删除操作时,LinkedList的效率相对较高,因为ArrayList在进行插入、删除操作时,会对数据的下标索引产生影响。

说说ArrayList的扩容机制

由于数组不可以扩容,ArrayList的底层机制为数组,所以ArrayList有一个自动扩容机制:
ArrayList的初始数组长度默认为10(可以指定其他长度),每次实际容量达到数组最大容量后,就会触发自动扩容(创建一个新数组,新数组容量将扩充为原容量的1.5倍,再把老数组里的数据拷贝到新的数组中(Arrays.copyOf()),最后把当前需要添加的元素添加到新数组中)。

ArrayList的扩容为什么是扩充为原容量的1.5倍?

ArrayList扩容大小为原大小的1.5倍,这是一个经验性的选择,它试图在避免频繁扩容(这会导致时间和内存开销)和避免浪费过多内存之间找到一个平衡点。当扩容因子较小的时候,因为每次增加的数组空间很小,所以容易出现频繁扩容的现象,这会大大增加时间和内存的开销;而扩容因子过大的时候,每次扩容新增的数组空间会很多,也可能出现数组空间太大导致空间利用率低浪费空间的现象。所以综合以上考虑,1.5倍是一个折中平衡的选择。

Hash的基本概念

Hash的基本概念是把任意长度的输入通过一个hash算法后,映射成固定长度的输出。由于是把任意长度的输入转换为固定长度的输出,所以实际情况中,可能碰到两个value值通过hash算法之后所得到的输出为相同的hash值,也就是会发生hash冲突,好的哈希算法应该尽量使hash值排列松散,从而减少冲突的概率

HashMap和HashTable的区别

从功能特性来看:
1.HashTable线程安全的,而HashMap不是
2.HashMap的性能比HashTable好,因为HashTable采用了全局同步锁来保证线程安全性,这对HashTable的性能影响很大
从内部实现的角度来看:
1.HashTable的底层数据结构使用的是数组加链表,HashMap则采用数组+链表+红黑树(从jdk8开始,对于HashMap,只要满足链表长度超过8,数组长度大于64时,链表存储将转化为红黑树存储,以提升性能,当红黑树的结点小于6时,红黑树将退化为链表);
2.HashMap初始容量为16,HashTable初始容量为11
3.HashMap可以使用null作为key,而HashTable不允许。

HashMap的put()方法执行流程

HashMap的put()方法用于将指定的键值对映射到HashMap中。以下是put()方法的执行流程:
1.计算键的哈希值:
首先,put()方法会计算键的哈希值,使用键的hashCode()方法来获取,从而确定键值对在HashMap中的存储位置。
2.计算哈希桶索引:
将计算得到的哈希值通过一系列位运算,确定该键值对在哈希表中的存储位置,即哈希桶(数组中的每个元素称为一个桶)的索引。
3.定位哈希桶:
使用计算得到的哈希桶索引,定位到HashMap内部的数组中的相应位置,该位置就是键值对的潜在存储位置。
4.处理冲突:
如果发现在计算得到的位置已经存在其他键值对,这就是
哈希冲突
HashMap采用链表或红黑树的方式来处理冲突。如果当前位置上是链表,新的键值对将被添加到链表的末尾;如果当前位置上是红黑树,将通过红黑树的插入操作来完成。
5.检查是否需要进行扩容:
在添加新键值对后,HashMap会检查当前元素数量是否超过了扩容阈值(扩容阈值为:当前最大容量扩容因子,扩容因子默认为0.75)。如果超过了阈值,HashMap会进行扩容操作,重新计算哈希桶的大小,重新分配元素,以保持哈希表的性能。
6.返回旧值:
如果在执行put()时替换了已存在的键值对,put()方法会返回被替换的键对应的旧值;否则,返回null。

HashMap的扩容因子为什么默认值是0.75?

扩容因子表示Hash表中的元素填充程度,扩容因子的值越大,就意味着触发扩容的元素个数会更多,虽然它的整体空间利用率比较高,但是发生Hash冲突的概率也会增加,反过来说,扩容因子的值越小,那么触发扩容元素的个数也就越少,这意味着出现Hash冲突的概率也会减少,但是对于内存空间的浪费就会更多,并且还会增加扩容的频率。
因此,扩容因子的值的设置本质上就是Hash冲突的概率以及空间利用率直接的平衡。0.75来源于统计学中的泊松分布,HashMap中是采用链式寻址的方式解决Hash冲突的问题,而为了避免链表过长导致时间复杂度增加,所以当链表长度大于8的时候,就会转化为红黑树从而提升检索的效率,当扩容因子为0.75时,链表长度超过8的可能性很小,从而较好地达到空间成本与时间成本的平衡

JDK1.7到JDK1.8HashMap 发生了什么变化(底层)?

1.JDK1.7中底层是数组+链表,JDK1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率。
2. JDK1.7中链表插入使用的是头插法,JDK1.8中链表插入使用的是尾插法,因为JDK1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,使用尾插法可以遍历链表元素。
3. JDK1.7中哈希算法比较复杂,存在各种右移与异或运算,JDK1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而JDK1.8中新增了红黑树,相比链表能有更高的散列性,所以可以适当的简化哈希算法,以节省CPU资源。

ConcurrentHashMap的底层实现原理是什么?

在Java8中,ConcurrentHashMap是由数组、单向链表和红黑树来构成的,当我们初始化一个ConcurrentHashMap实例的时候,默认会初始化一个长度等于16的数组,由于ConcurrentHashMap的核心依然是Hash表,所以必然会存在Hah冲突的问题,所以ConcurrentHashMap采用了链式寻址的方式来解决Hash表的冲突。
当Hash冲突比较多时,会造成链表长度过长的问题,这会使得ConcurrentHashMap中的数组元素查询负责度增加,所以在Java8里引入了红黑树来优化性能,当数组长度大于64并且链表长度大于等于8的时候,单向链表就会转化成红黑树,另外随着ConcurrentHashMap的动态扩容,一旦链表长度小于6,红黑树就会自动退化成单向链表。
ConcurrentHashMap本质上就是一个特殊的HashMap,因此功能和HashMap是基本一致的,但是ConcurrentHashMap在HashMap的基础上提供了并发安全的实现,并发安全的实现主要是通过对于链表Node节点加锁来保证线程安全。
ConcurrentHashMap做了几个性能的优化,主要体现在以下几个方面:
1.在Java8中,ConcurrentHashMap的锁粒度是数组中的某一个节点,而在Java8之前,它锁定的是Segment,锁的范围相对要更大,所以性能上会更低;
2.引入红黑树机制,从而降低了数据查询的时间复杂度,红黑树的时间复杂度是O(logn),这是因为每次查询操作都会将搜索空间减半,直到找到目标元素或确定元素不存在为止。而相比之下,链表查询的时间负责度是O(n);
3.当数组长度不够时,ConcurrentHashMap需要对数组进行扩容,而在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容的实现,简单来说,就是多个线程对原始数据进行分片,分片之后,每个线程去负责一个分片的数据迁移,从而提升扩容过程中数据迁移的效率;
4.ConcurrentHashMap有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下去实现元素个数的累加,它的性能时非常低的,所以ConcurrentHashMap对此作了一些优化:当线程竞争不激烈时,直接采用CAS乐观锁的方式来实现元素个数的原子递增,如果线程竞争比较激烈时,使用一个数组来维护元素个数,如果要增加总的元素个数,直接从数组中随机选择一个,再通过CAS算法来实现原子递增,从而通过引入数组来实现对并发更新的负载。

谈谈ConcurrentHashMap的扩容机制

JDK1.7和JDK1.8对于ConcurrentHashMap的扩容机制是不一样的。

对于JDK1.7版本:

  1. JDK1.7版本的ConcurrentHashMap是基于Segment分段实现的;
  2. 每个Segment相当于⼀个小型的HashMap;
  3. 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似;
  4. 先⽣成新的数组,然后转移元素到新数组中;
  5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值。

对于JDK1.8版本:

  1. JDK1.8版本的ConcurrentHashMap不再基于Segment实现;
  2. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程⼀起进行扩容;
  3. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容;
  4. ConcurrentHashMap是支持多个线程同时扩容的;
  5. 扩容之前也先生成⼀个新的数组;
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作。

下一篇传送门点我

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值