爪哇基础题08

今天做了10道题,发现一个有意思的事情,用手机做java专项训练就是一次10题,用电脑做就是一次5题。今天的题涉及到的内容真挺多的。

1.ArrayList和LinkList的描述,下面说法错误的是?(D)

A LinkedeList和ArrayList都实现了List接口

B ArrayList是可改变大小的数组,而LinkedList是双向链接串列

C LinkedList不支持高效的随机元素访问

D 在LinkedList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在ArrayList的中间插入或删除一个元素的开销是固定的

做这题就是要明白ArrayList和LinkedList各自都有什么特点。知识点有点忘,我就系统学习了一下这两个集合的基本内容以及底层源码分析,源码分析真不容易呀。

学习ArrayList和LinkedList之前,我们先来了解一下两种数据结构:数组和链表,对我们理解这道题有帮助

数组:查询数据通过索引定位,查询任意数据耗时相同,查询效率高。删除数据时,要将原始数据删除,而且后面每个数据前移,删除效率低。添加同理,效率低

链表:链表是由结点构成,一个节点中包含实际存储的数据以及下一个节点的地址。结点存储地址的位置用"^"表示节点没指向下一个节点,链表结束。

在数据AC之间添加数据B,该怎么做:

  1. 数据B对应的下一个地址指向数据C
  2. 数据A对应的下一个地址指向数据B

删除BD之间的数据C,该怎么做:

  1. 数据B对应的下一个地址指向数据D
  2. 删除数据C

查询数据D是否存在,必须从头(head)开始查找。查询第三个数据,必须从头(head)开始查找。

ArrayList:

ArrayList实现了List接口,是可调整大小的数组,特点是增删慢,查询快。

下面输出的结果是"[]"。此时我们创建的是ArrayList对象,而ArrayList是java写好的一个类,这个类在底层做了一些处理,打印的不是对象地址值,而是集合中存储的数据内容。此时集合是空的,长度为0。

ArrayList<String> list = new ArrayList<>();
System.out.println(list);//[]
//两种情况
//一次添加一个
ArrayList<String> list1 = new ArrayList<>();
list1.add("aaa");
list2.add("bbb");
list3.add("ccc");

//一次添加多个
ArrayList<String> list2 = new ArrayList<>();
list2.addAll(list1);

底层原理:ArrayList底层是数组结构

  1. 利用空参创建的集合,在底层创建一个默认长度为0的数组
  2. 添加第一个元素时,底层会创建一个新的长度为10的数组
  3. 存满时,会扩容1.5倍,再不够继续扩容1.5倍
  4. 如果一次添加多个元素,1.5倍还放不下,则新创建的数组的长度以实际情况为主

创建的数组名为elementData,还有一个size变量,size有两层含义,一是元素个数,二是下次存放的位置

查看原码的快捷方式,ctrl+n输入ArrayList,弹出框右上角选择All Places

进入后底层源码多,看着不方便,我们可以把大纲视图展示出来,alt+7

可以把大纲放到右边

private int size;

//空参构造
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//长度为0的数组
    }

空参构造,elementData就是创建出的数组,默认长度为0。还有变量size,为数组的长度。

这是add的原码操作,首先通过空参构造创建集合,默认长度为0。添加第一个元素,进入到第一个add中,其中还有一个add(),俩面有三个参数

  • 参数一:当前要添加的元素
  • 参数二:集合底层的数组名称
  • 参数三:集合的长度以及下一个元素应该被添加到的位置

在下一个add中,先判断s就是要添加的位置是不是跟数组的长度相等,第一次添加都是0,相等,要执行grow(),也即是数组扩容,将现有的个数+1,再调用有参的grow(),minCapacity = 1,是最小容量的意思。然后就是进行判断,第一次添加数据会执行到else,在后面扩容的话就会执行if中的方法。DEFAULT_CAPACITY = 10,minCapacity = 1,最后就创建一个长度为10的数组,依次返回就回到了add()中,grow()执行完毕。然后把元素添加到数组中,而且要让size后移,代表下一次要添加的位置。

以上是添加的数据不会超过10的情况下,那要是超过了呢,原来有10个,再来一个就11个了。

当添加第11个时,底层要进行扩容,依次执行add(),执行到第二个add()进行判断时,发现数组长度和s相等了,也就是存满了,就要执行grow()进行扩容。到了grow()再进行if判断,此时minCapacity = 11,表示添加完当前元素之后的最小容量,这次就进入了if中,因为oldCapacity = 10,满足if条件。在if中要执行

int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity-oldCapacity,oldCapacity>>1);
//minCapacity-oldCapacity = 1 表示理论上我们至少要新增的容量
//oldCapacity右移一位,表示默认新增容量的大小

在newLength()中进行比较,Math.max(minGrowth,prefGrowth);也就是理论上至少要新增的容量和默认要新增的容量(5)谁大。为什么做这样的比较,因为在集合中,不仅仅可以一次添加一个元素,还可以一次添加很多个元素。

int prefLength = oldLength + Math.max(minGrowth,prefGrowth);//集合中新数组真正的长度
//第一种情况:如果一次添加一个元素,那么第二个参数一定是1,表示此时数组只要扩容1个单位就可以了。
//第二种情况:如果一次添加多个元素,假设100,那么第二参数就是100,表示此时数组扩容100个单位才可以

将新数组长度返回到grow()中,然后执行return,需要拷贝,根据copyOf()的第二个参数创建一个新数组,并且把老数组拷贝过去。最后一层层返回到add()中

LinkedList:实现List接口,特点是增删快,。底层数据结构是双向链表,查询慢。增删快,但是如果操作的是首尾元素,速度也是极快的。每一个节点都是三部分,第一部分用来记录前一个节点的地址值,第二部分用来记录真实数据,第三部分用来记录后一个节点地址值。

分析底层源码:

/*

private static class Node<E>

*/

Node表示节点,Node对象就是节点对象,具有三个成员变量,对应节点的三部分。

private static class Node<E> {
        E item;
        Node<E> next;//后一个节点地址
        Node<E> prev;//前一个节点地址

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

来看一下add()方法的底层源码,然后在看一下linLast()方法,顺便过一下使用add()方法的过程

LinkedList<String> list = new LinkedList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
-----------------------------------------------------
//LinkedList<E>类中的成员方法和成员变量
int size = 0;
Node<E> first;//头结点 默认初始化为null
Node<E> last;//尾结点	默认初始化为nul
public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

//表示一个节点
private static class Node<E> {
   E item;
   Node<E> next;//后一个节点地址
   Node<E> prev;//前一个节点地址

   Node(Node<E> prev, E element, Node<E> next) {
       this.item = element;
       this.next = next;
       this.prev = prev;
   }
}

执行这段代码,肯定是现在堆内存中开辟一个内存空间,用来存方法一个LinkedList对象

然后执行list.add("aaa");在add()中又会调用linkLast(e)方法,传入参数就是"aaa"。在这个方法中,最初l = null,然后创建newNode对象,创建一个节点,节点的第一部分和第三部分都是null,第二部分就是"aaa"。这个节点在堆内存中也会开辟一个内存空间,地址就假设为0x0011。把这个地址赋给last,这样这个LinkedList的尾结点就指向了这个节点。

接着判断,l == null正确,就把头结点也指向了0x0011。size++就代表集合长度多了一个

现在又添加了一个,叫做"bbb"。此时linkLast(e)中的l就不是null了,而是0x0011,然后创建新Node,地址假设为0x0022,,第一个位置是0x0011,第二个位置是"bbb",第三个位置是null。现在这个Node的头就指向了0x0011的尾。

再把newNode给last,也就是把0x0022给了last,尾结点。接着判断,l已经不是null了,执行下一步,0x0011.next() = 0x0022。就是把0x0011的节点第三部分变成0x0022

最后添加"ccc"

这道题差不多完事儿,答案现在一看就知道选什么了吧,借此机会多学了点东西,还是很有意义的。其实也偷了点懒,ArrayList的底层没整理,因为我看的网课讲的jdk版本和我的不一样,导致底层源码不一样,所以就没整理,下次一定

2.下列java程序的输出结果为____。(B)

public class Example{
    String str=new String("hello");
    char[]ch={'a','b'};
    public static void main(String args[]){
        Example ex=new Example();
        ex.change(ex.str,ex.ch);
        System.out.print(ex.str+" and ");
        System.out.print(ex.ch);
    }
    public void change(String str,char ch[]){
        str="test ok";
        ch[0]='c';
    }
}

A hello and ab

B hello and cb

C hello and a

D test ok and ab

E test ok and cb

F test ok and c

可恶,昨天刚做完类似的题,也是这种传参的,考察值传递和引用传递,今天又整混了。

首先我们考到了通过new创建了一个String对象,那这个对象就存储在了堆中,这是一个引用类型,变量a存储在栈中,指向堆中的"hello"。然后把a传递到方法中,传递的是地址,这是引用传递,形参str也指向了堆中的"hello"。但是,在方法中又要给"str"赋值,这是String类,str="test ok"相当于又重新创建了一个对象,对象存储在常量池中,str存储在栈中指向"test ok"。因此,此方法并不能改变传入的str。这个字符串数组传递也是引用传递,方法形参接收到了ex.ch的地址,并完成修改。

3.Java1.8版本之前的前提,Java特性中,abstract class和interface有什么区别(ABD)

A 抽象类可以有构造方法,接口中不能有构造方法

B 抽象类中可以有普通成员变量,接口中没有普通成员变量

C 抽象类中不可以包含静态方法,接口中可以包含静态方法

D 一个类可以实现多个接口,但只能继承一个抽象类。

这题考察的其实是abstract和interface的各自特点,也是我一直有点不清楚的地方,它们的思想是什么,什么时候使用抽象类什么时候使用接口。下面我就来写写两者的思想和注意事项,基本的东西就不多说了。

abstract:abstract产生于父子类继承的关系,比如现在有一个Student类和Teacher类,他们都继承于Person类,Person类包含了Student和Teacher的共性方法。但是有个问题,Person中有一个work()方法,但是Student和Teacher的工作是不一样的,无法确定方法体,因此我们就有了重写机制,随便在父类work()方法中写点东西,再由子类重写方法,但是如果子类交给别人写呢,他忘了重写或者他就是不想重写,那可怎么办,于是便有了抽象类abstract,就是专门治他们而存在的。在父类中不确定方法体,那好啊,就不写方法体了,再加上一个abstract修饰,这就成了一个抽象方法,强制子类要写抽象方法,抽象方法所在的类就是抽象类。

注意事项:

  • 抽象类不能实例化,也就是说不能创建对象。假如你非要创建了一个对象,那去调用抽象类的方法,这个方法连方法体都没有,调用它有什么意义,所以不能创建该对象。
  • 抽象类中不一定有抽象方法,有抽象方法的一定是抽象类
  • 可以有构造方法,可以有成员变量。但是抽象类也不能创建对象,那为什么还要有构造方法呢?其实是子类创建对象时,会用到父类的属性,并通过父类的构造方法(super)来给属性赋值。
  • 抽象类的子类
  • 要么重写抽象类中的所有抽象方法
  • 要么子类也是抽象类

抽象类和抽象发放的意义:

  • 疑问:
  • 把子类共性的内容抽取到父类后,
  • 由于方法体不确定,需要定义为抽象。子类需要重写。
  • 那我不抽取到父类,直接在子类写不是更节约代码吗?

公司里代码都不是一个人来写。假如现在有Cat类和Dog类,父类是Animal类,子类都有eat()方法和drink()方法,我们只把drink()方法抽取到了Animal中而没有抽取eat()方法。现在程序员A来开发Cat类,他英语不好,就把eat()方法改名为abc,程序员B开发Dog类,他没有改名,继续用eat()方法,这都没问题。但是现在程序员C来了,他要调用吃东西的方法,那这样他就没法调用了,根本不知道该调用谁,只能去子类中看看,那可太麻烦了。所以我们要把共性的东西都抽取到父类,然后强制子类重写。

接口:为什么要有接口呢?动物类中已经有了子类包括兔子类、青蛙类和狗类,然后把吃饭喝水两个共性方法提取到了父类,现在想多个功能,游泳,他能抽取到父类中吗?答案是不能的。狗和青蛙会有用的,兔子不会,所以游泳写在父类不合适,写在各自子类中又会不统一。因此我们创建一个接口,里面有swim()方法,这个方法也是抽象的,然后哪个子类要游泳,哪个子类就去实现接口就行。也就是说当我们需要给多个类同时确定规则的时候,就需要用接口,接口不代表事务,是一种规则,对行为的抽象。

抽象类和接口总混,抽象类更多用在父类中,在抽取共性方法的时候,方法体不一样,就可以写成抽象方法。而接口不是一类事物,侧重行为,是一种规则,游泳就是行为,不仅Animal类可以实现,Person类也可以实现。

注意:

  • 接口不能实例化
  • 接口和类的实现关系,可以单实现也可以多实现
  • 实现类可以在继承一个类的同时,实现多个接口
  • 接口和接口之间可以单继承,也可以多继承。实现类实现的是最下面的子接口,就需要重写所有方法

接口中成员的特点:

  • 成员变量

只能是常量,默认修饰符为:public static final。子类共有属性都被抽取到父类中,所以接口中是不会有age、name这种成员变量的。而且接口是一种规则,规则是不能发生改变的,所以说接口中的成员变量都是final修饰的。static是为了方法调用

  • 构造方法

没有,因为接口不用创建对象,而且也不需要给子类成员变量赋值

  • 成员方法

只能是抽象方法,默认修饰符为public static。JDK7以前接口中只能定义抽象方法,JDK8的新特性是接口中可以定义有方法体的方法,JDK9的新特性是可以定义私有方法。

JDK8之后,为了升级扩展接口功能,可以新增有方法体的方法。

允许在接口中定义默认方法,需要使用关键字default修饰。默认方法不是抽象方法,所以不强制被重写。但是如果被重写,子类在重写的时候要去掉default。这么做的好处是想升级接口,就可以定义一个非abstract方法,等什么时候想重写该方法就什么时候重写。

默认方法注意事项:

  • public可以省略,default不能省略
  • 如果实现了多个接口,多个接口中存在相同名称的默认方法,子类必须对该方法进行重写。创建实现类对象,该对象可以直接调用接口中的默认方法,你如果不重写重名方法,就不知道调用的是哪个接口的方法了。

允许在接口中定义静态方法,用static修饰

静态方法注意事项:

  • 静态方法只能通过接口名调用
  • public可以省略,static不可省略

JDK9之后新增方法private方法,分为普通(默认)的私有方法和静态的私有方法

总结一下接口的思想和应用:

1.接口代表一种规则,是行为的抽象。如果想让哪个类拥有一种行为,就可以让这个类实现接口。

2.当一个方法的参数是接口时,可以传递这个接口的所有实现类,这就是接口的多态

真爽啊,一道题涉及到这么多知识点,看着简单但是真正能讲出来可不容易,学完这些这道题选啥可太容易了。

4.下面关于变量及其范围的陈述哪些是不正确的()

A 实例变量是类的成员变量

B 实例变量用关键字static声明

C 在方法中定义的局部变量在该方法被执行时创建

D 局部变量在使用前必须被初始化

这题难点在于C,C是有关JVM的内容。先给出结论,局部变量不是在方法执行的时候创建,而是在该变量被声明并赋值的时候创建,简单来说就是代码执行到了创建变量且给变量赋值的时候这个变量才会被创建。

我们都知道栈中存储着局部变量,同是类中定义的方法也会存储在栈中,方法有那么多,而且每个方法又包含了很多局部变量,那具体怎么存储呢?其实java虚拟机为了区分不同方法中局部变量作用范围的内存区域,每个方法在运行的时候都会分配一块独立的栈帧内存区域,如下图所示。

栈帧中也不仅仅存储局部变量,还会存其他的,如下图所示。

以comput()方法为例,看看对应的栈帧具体流程(具体是由jvm的字节码文件进行研究,这里不做展开了,感兴趣的小伙伴可以自己看看,参考内容:https://zhuanlan.zhihu.com/p/98337005?time=1673178070752)。

public class Demo {
	
	public void comput() {
		int a;
		int b = 5;
		int c = b + 4;
		a = 2;
	}
	
	public static void main(String[] args) {
		Demo demo = new Demo();
		demo.comput();
	}
}

比如执行int b = 5;这句代码,首先会将一个常量5压入操作数栈中,然后在局部变量表中给b变量分配内存,最后将操作数栈中的5给b。然而执行int a;的时候,jvm不会做任何操作,直到执行了赋值语句a = 2;之后,才会在局部变量表中创建变量并赋值,也验证了“局部变量是在赋值的时候创建的结论”。

#你的秋招进展怎么样了##我的2023新年愿望##你们的毕业论文什么进度了##你觉得一个人能同时学好硬件和软件吗##硬件人如何看待稚晖君从华为离职#
java基础知识 文章被收录于专栏

我是一个转码的小白,平时会在牛客中做选择题,在做题中遇到不会的内容就会去找视频或者文章学习,以此不断积累知识。这个专栏主要是记录一些我通过做题所学到的基础知识,希望能对大家有帮助

全部评论

相关推荐

头像
04-26 15:00
已编辑
算法工程师
点赞 评论 收藏
转发
34 3 评论
分享
牛客网
牛客企业服务