Android面试心得

过年回来到现在也一个月了,这段时间一直没写文章,这是因为我准备换工作了,一直在面试,也面试了四五家,但是效果都不是很好,虽然如此,但也算收获了一些经验,我就将我面试遇到的问题记录下来,与大家一起分享吧。(本人是做游戏sdk的,所以一些问题会偏向于sdk的,如果不找sdk方向的工作可以忽略其中的一些问题)

一、面试基础

1、自我介绍
这个大家自己可以好好看一下网上的一些攻略,自己组织一个好一点的自我介绍,主要是要把个人信息,之前做过什么介绍清楚,这个就看自我发挥了。

2、之前做过的项目或者工作经历,遇到了什么难点,解决了什么问题,技术上得到了哪些提高(这个必问,希望大家准备好)
这个问题是必问的,而且感觉还挺重要的,大家面试前先准备好几个技术难点,哪怕项目中没用过也没关系。

3、介绍一下之前项目中常用的第三方框架

二、java部分

一、ArrayList和HashMap实现原理,他们的特性是什么。

ArrayList:底层数据结构是Object数组,查询快,增删慢,查询是根据数组下标直接查询速度快,增删需要移动后边的元素和扩容,速度慢。线程不安全,元素单个,效率高。(如果要线程安全的List集合可以用Vector)

HashMap:jdk1.8之前的数据结构是数组+链表,1.8之后是数组+链表+红黑树,当链表长度小于8的时候使用的还是数组+链表,当链表长度超过8时转换为红黑树,HashMap元素成对,元素可为空,线程不安全,new HashMap的时候初始化默认大小为16。(如果要线程安全的Map集合可以用HashTable或ConcurrentHashMap)

参考资料

一、String和StringBuffer、Springbuilder。

  1. 三者在执行速度方面的比较:StringBuilder > StringBuffer > String
    String:字符串常量(不可变的)
    StringBuffer:字符串变量(可变的)
    StringBuilder:字符串变量(可变的)
    参考资料

二、java四大引用

强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象

软引用(SoftReference):只有在内存空间不足时,才会被回收的对象

弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

三、java抽象类和接口,有什么区别

抽象类:在Java中被abstract关键字修饰的类称为抽象类,被abstract关键字修饰的方法称为抽象方法,抽象方法只有方法的声明,没有方法体。抽象类的特点:

a、抽象类不能被实例化只能被继承;

b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;

c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;

d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;

e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

接口:Java中接口使用interface关键字修饰,特点为:

a、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);

b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;

c、一个类可以实现多个接口;

d、JDK1.8中对接口增加了新的特性:(1)、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。

相同点

(1)都不能被实例化 (2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。

不同点

(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。

(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。

(3)接口强调特定功能的实现,而抽象类强调所属关系。

(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。

四、设计模式六大原则,能否给我说说Android中至少3个用到设计模式的例子
1、单一职责原则:有且只有一个原因会引起类的变化。
2、接口隔离原则。
3、里氏替换原则。
4、依赖倒置原则。
5、迪米特法则。
6、开闭原则。

23种设计模式:
创建型5个:单例模式、建造者模式、原型模式、工厂方法、抽象工厂

行为型11个:策略模式、状态模式、观察者模式、中介者模式、访问者模式、迭代器模式、模板方法、备忘录模式、命令模式、解释器模式、责任链模式

结构型模式7个:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式

OKHttp内部采用责任链模式来完成每个interceptor拦截器的调用
定义:使得多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。将 这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理该请求为止。
优点: 将请求者和处理者关系解耦,使代码更加灵活。
缺点: 在链中遍历寻找请求处理者中,如果处理者太多,会影响性能。

RxJava的观察者模式
定义:定义对象间一种一对多的依赖关系,使得每当一对象状态发生改变时,所有依赖它的对象 会得到通知并自动更新。
应用:listView的Adapter的notifyDataSetChanged方法,广播,事件总线机制。
观察者模式主要的作用是对象解耦,将观察者和被观察者分隔开,只依赖于observer(观察员) 和observable(可观察的)抽象。
优点: 1.观察者和被观察者是抽象耦合,以应对业务变化; 2.增强系统的灵活性和可扩展性。
缺点: 在Java中通知是顺序执行,如果观察者卡顿,会影响整体的执行效率。这种情况下,一般建议采 用异步的方式。

listview/gridview的适配器模式
1.使用场景: 1.接口不兼容 2.想建立一个可以重复使用的类 3.需要统一的输出接口,而输入端类型不可知。
优点: 1.更好的复用性:复用现有的功能; 2.更好的扩展性:扩展现有的功能。
缺点: 过多的使用适配器,会使系统凌乱不堪,不易于整体把握。如:明明调用的是A接口,内部适配 的却是B接口的实现,如果系统出现太多这种情况,无异于一场灾难。

Context外观模式
使用场景: 为复杂子系统提供一个简单接口。
优点: 1.隐藏子系统实现细节,减少客户和子系统的耦合,能够拥抱变化; 2.外观类对子系统接口封装,使子系统更加容易使用。
缺点: 1.外观类接口膨胀; 更多免费Android开发视频课程,2.外观类没有遵循开闭原则,当业务出现变更时,可能需要修改外观类。

AlertDialog、Notification 源码使用了建造者模式完成参数的初始化
优点:1.良好的封装性,隐藏内部实现细节; 2.建造者独立,容易扩展。
缺点: 会产生多余的Builder对象和Director对象,消耗内存。

安卓应用主题是抽象工厂模式的最好体现
安卓应用有两套主题:LightTheme亮色主题和DarkTheme暗色主题。主题之下有各种与之相关的 UI元素。创建主题也要随之创建各种UI元素,这就是抽象工厂模式的最好体现。
优点: 分离接口和实现,面向接口编程。在具体实现中解耦,同时基于接口和实现的分离,使得产品类 切换更加容易,灵活。
缺点: 1.类文件爆炸性增加 2.新的产品类不易扩展

参考文章

五、一个线程几个loop
一个线程只有一个looper,但是一个线程可以有多个handler
参考文章

六、线程死锁问题

七、try catch与finally
1.不管有没有出现异常,finally中的语句块都会执行,finally语句通常用来释放资源,关闭数据库,关闭文件等操作
2.当try和catch中出现return时finally仍然会执行
3.finally是在return后面的表达式之后执行的。此时并没有将值返回,而是将数据保存在局部变量中,待finally中语句执行完成后返回。

参考文章

八、写出常见的几种单例模式
1、饿汉式,线程安全

public class Singleton {
    
    private static Singleton instance = new Singleton();

    // 私有构造,不允许外部通过构造实例化 Singleton.class
    private Singleton() {
    }

    public static Singleton newInstance() {
        return instance;
    }

}

2、懒汉式,线程不安全

public class Singleton {
    
    private static Singleton instance;

    // 私有构造,不允许外部通过构造实例化 Singleton.class
    private Singleton() {
    }

    public static Singleton newInstance() {
        return instance= new Singleton();
    }

}

3、懒汉式,线程安全

public class Singleton {
    
    private static Singleton instance;

    // 私有构造,不允许外部通过构造实例化 Singleton.class
    private Singleton() {
    }

    public static synchronized Singleton newInstance() {
        return instance= new Singleton();
    }

}

4、双重检查模式 DCL

public class Singleton {
    
    private static Singleton instance;

    // 私有构造,不允许外部通过构造实例化 Singleton.class
    private Singleton() {
    }

    public static Singleton newInstance() {
        if(instance==null){
           synchronized(Singleton.class){
              if(instance==null){
                 instance = new Singleton();
              }
           }
        }
        return instance;
    }

}

5、静态内部类,跟饿汉式差不多,但是通过静态内部类实现了懒加载

public class Singleton {

    // 私有构造,不允许外部通过构造实例化 Singleton.class
    private Singleton() {
    }
    private static class SingletonHelp{
       private final static Singleton instance = new Singleton();
    } 

    public static Singleton newInstance() {
        return SingletonHelp.instance;
    }

}

因为jvm加载类的时候,进行类的初始化,会去获取一个锁,所以加载静态类的时候,只有一个线程能获取到初始化锁,当别的线程获取到初始化锁的时候,此时静态内部类的静态对象已经初始化完毕,其他线程无需再初始化了,所以达到了线程安全的效果,确保只创建一个对象。

参考文章

三、Android部分

1、65535错误是怎么回事

该错误主要是由于我们打包后classes.dex文件里方法数量超出的65535个。
预防方法主要是少写方法,多用基类,能不用兼容包的就不要用兼容包的,引入jar或library工程时注意下它们的方法数,但是如果实在没办法,就用这个方法解决:

第一步:在项目的grade文件里面的defaultConfig闭包下添加: multiDexEnabled true

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        multiDexEnabled true

第二步:在dependencies下添加依赖 compile ‘com.android.support:multidex:1.0.0’

dependencies {
    compile 'com.android.support:multidex:1.0.0'

第三步:自定义继承于Application的类,并重写protected void attachBaseContext(Context base)方法,调用 MultiDex.install(this)初始化,最后记得在Manifest清单里注册自定义的application类,如图:

public class MyApplication extends Application{

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

该问题的参考文章一

该问题的参考文章二

2、什么是内存泄漏、什么是内存溢出,该怎么应对

内存溢出(OOM):指的是申请不到足够的内存;

原因:1、内存一次性开销过大(加载巨图)。2、内存持续性开销(循环创建对象)。3、内存回收不及时(内存开销过快,GC频率跟不上开销速度等)。4、内存无法回收(内存泄漏导致内存溢出)。

解决方案:1、调整图像大小。2、尽可能不在循环中申请内存。3、及时回收图像。

参考文章地址

内存泄露(Leak):无法释放已申请的内存;

在Java中,判断对象是否仍在使用的方法是:引用计数法,可达性分析。

原因:1、静态变量(单例模式)。2、监听器。3、内部类(内部类持有外部引用)。4、资源对象未关闭。5、容器中的对象没有清理。6、webview。

避免内存泄漏方法:
1、不要在匿名内部类中进行异步操作
2、将非静态内部类转为静态内部类 + WeakReference(弱引用)的方式
3、使用Context时,尽量使用Application 的 Context
4、尽量避免使用static 成员变量。另外可以考虑lazy初始化
5、为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放
6、及时关闭资源。Bitmap 使用后调用recycle()方法

参考文章一
参考文章二

两者关系:内存泄露 → 剩余内存不足 → 后续申请不到足够内存 →内存溢出。

3、什么是Android的模块化、组件化、插件化

模块化:一个程序按照其功能做拆分,分成相互独立的模块(例如:登陆,注册)。模块化的具体实施方法分为插件化和组件化。

组件化:是将一个app分成多个模块,每个模块都是一个组件(module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件,但是最终发布的时候将这些组件合并成一个统一的apk,这就是组件化开发。
插件化:插件化开发和组件化不同,插件化开发就是将整个app拆分成很多模块,每个模块都是一个apk(组件化的每个模块是一个lib),最终打包的时候将宿主apk和插件apk分开打包,插件apk通过动态下发到宿主apk,这就是插件化。

参考文章地址

4、浅述APK的打包流程

第一步 aapt阶段: 资源打包工具,将res资源文件打包成R.java文件和res文件。
第二步 aidl阶段: 这个阶段处理.aidl文件,生成对应的Java接口文件。
第三步 Java Compiler阶段:通过Java Compiler编译R.java、Java接口文件、Java源文件,生成.class文件。
第四步 dex阶段: 通过dex2.jar将.class文件处理生成class.dex。
第五步 apkbuilder阶段:将classes.dex、res文件夹、AndroidManifest.xml打包成apk文件。
第六步 Jarsigner阶段:对apk文件加签,形成一个可以运行的apk。

参考文章地址一
参考文章地址二

5、打apk包时,V1、V2、V3签名的区别是什么

v1签名是对jar进行签名,V2签名是对整个apk签名:官方介绍就是:v2签名是在整个APK文件的二进制内容上计算和验证的,v1是在归档文件中解压缩文件内容,V3签名和V2差不多,也是全文件签名方式,只不过V2是安卓7.0引入,V3是安卓9.0引入。

二者签名所产生的结果:
v1:在v1中只对未压缩的文件内容进行了验证,所以在APK签名之后可以进行很多修改——文件可以移动,甚至可以重新压缩。即可以对签名后的文件在进行处理
v2:v2签名验证了归档中的所有字节,而不是单独的ZIP条目,如果您在构建过程中有任何定制任务,包括篡改或处理APK文件,请确保禁用它们,否则您可能会使v2签名失效,从而使您的APKs与Android 7.0和以上版本不兼容。

根据实际开发的经验总结:

    一定可行的方案: 只使用 v1 方案
    不一定可行的方案:同时使用 v1 和 v2 方案
    对 7.0 以下一定不行的方案:只使用 v2 方案

然后Android不支持V3版本的签名,所以在AS里面看不到V3。但是在SDK中有个签名工具apksigner.jar。只有9.0以上这个签名工具才能签V3版本的签名。所以Android studio签不了V3,必须使用apksigner.jar工具来签V3。
然后V2、V3区别就是:
一,V3签名多了一个判断机制:“APK签名数据块大小必须是4096的倍数”
二,V3签名分块采用V2相同的签名分块格式,只不过改了V2签名分块中的那个ID
三,增添了有关受支持的SDK版本和prof-of-rotation结构的信息

参考文章地址
参考文章地址
参考文章地址

6、Handler

什么是handler:
Handler是Android SDK来处理异步消息的核心类。
子线程与主线程通过Handler来进行通信。子线程可以通过Handler来通知主线程进行UI更新。

参考文章地址

7、ListView和RecylerView的区别,以及如何优化

1、 缓存不同:
ListView是2级缓存,RecyclerView比ListView多两级缓存,支持多个离ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。

2、adapter不同
ListView有自带的Adapter,例如ArrayAdapter等,而RecylerView所有的adapter必须由自己实现。

3、布局不同
ListView布局较为单一,只有纵向布局,RecylerView横向、纵向、表格、瀑布流都可以实现。

4、刷新区别
ListView中通常使用全局刷新函数notifyDataSetChanged()来刷新ListView中的所有数据,这是一个非常耗费资源的行为,RecyclerView则可以实现数据的局部刷新,例如notifyItemChanged()函数等。

5、 动画区别:
在RecyclerView封装的类中已经自带了很多内置的动画API,而ListView则需要自己实现。

6、item点击事件:
ListView提供了setOnItemClickListener()这样的item点击事件,而RecylerView没有,需要自己实现。

ListView的优化:
优化方式一:
convertView的复用,进行布局的复用。
优化方式二:
ViewHolder的使用,避免每次都findviewById。
优化方式三:
使用分段加载。
优化方式四:
使用分页加载。

参考文章

RecylerView的优化:

参考文章

8、EventBus
EventBus是一种用于Android的事件发布-订阅总线,它简化了应用程序内各个组件之间进行通信的复杂度,尤其是碎片之间进行通信的问题,可以避免由于使用广播通信而带来的诸多不便。

三个角色:
Event:事件,它可以是任意类型,EventBus会根据事件类型进行全局的通知。
Subscriber:事件订阅者,在EventBus 3.0之前我们必须定义以onEvent开头的那几个方法,分别是onEvent、onEventMainThread、onEventBackgroundThread和onEventAsync,而在3.0之后事件处理的方法名可以随意取,不过需要加上注解@subscribe,并且指定线程模型,默认是POSTING。
Publisher:事件的发布者,可以在任意线程里发布事件。一般情况下,使用EventBus.getDefault()就可以得到一个EventBus对象,然后再调用post(Object)方法即可。

四个线程:
POSTING:默认,表示事件处理函数的线程跟发布事件的线程在同一个线程。
MAIN:表示事件处理函数的线程在主线程(UI)线程,因此在这里不能进行耗时操作。
BACKGROUND:表示事件处理函数的线程在后台线程,因此不能进行UI操作。如果发布事件的线程是主线程(UI线程),那么事件处理函数将会开启一个后台线程,如果果发布事件的线程是在后台线程,那么事件处理函数就使用该线程。
ASYNC:表示无论事件发布的线程是哪一个,事件处理函数始终会新建一个子线程运行,同样不能进行UI操作。

参考文章

9、ANR问题

ANR全称:Application Not Responding,也就是应用程序无响应。

以下四个条件都可以造成ANR发生:

InputDispatching Timeout:5秒内无法响应屏幕触摸事件或键盘输入事件
BroadcastQueue Timeout :在执行前台广播(BroadcastReceiver)的onReceive()函数时10秒没有处理完成,后台为60秒。
Service Timeout :前台服务20秒内,后台服务在200秒内没有执行完毕。
ContentProvider Timeout :ContentProvider的publish在10s内没进行完成。

造成ANR的原因及解决办法:
1、主线程阻塞或主线程数据读取。解决办法:使用子线程处理耗时任务或者阻塞任务
2、CPU满负荷,I/O阻塞。解决办法:文件读写或者数据库操作放在子线程。
3、内存不足。解决办法:优化内存,防止内存泄漏。
4、各大组件ANR。解决办法:各大组件的生命周期也应避免耗时操作。

参考文章一
参考文章二

10、android设备的唯一标识问题

没有IMEI授权就用android_id和MAC地址,但是android6.0之后MAC地址统一返回02:00:00:00:00:00。
有IMEI授权就用imei码。
android10不能用imei,用OAID吧。
以上的是主流的方法,还有MEID,ESN,IMSI等。
Android设备唯一标识

11、sdk打渠道包方法

12、APP从启动到看到第一个页面

13、look和synchronized的区别

类别synchronizedLock
存在层次Java的关键字,在jvm层面上是一个类
锁的释放会自动释放锁(1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁)需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式,具体下面会说到,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态无法判断可以判断
锁类型可重入、不可中断、非公平可重入、可判断、可公平(两者皆可)
性能适用于少量同步适用于大量同步
  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。

  2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。

  3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

  4. synchronized是隐式的加锁,lock是显式的加锁;

  5. synchronized可以作用于方法上,lock只能作用于方法块;

  6. synchronized底层采用的是objectMonitor,lock采用的AQS;

  7. synchronized是阻塞式加锁,lock是非阻塞式加锁支持可中断式加锁,支持超时时间的加锁;

  8. synchronized在进行加锁解锁时,只有一个同步队列和一个等待队列, lock有一个同步队列,可以有多个等待队列;

  9. synchronized只支持非公平锁,lock支持非公平锁和公平锁;

  10. synchronized使用了object类的wait和notify进行等待和唤醒, lock使用了condition接口进行等待和唤醒(await和signal);

  11. ock支持个性化定制, 使用了模板方法模式,可以自行实现lock方法;

14、OAID、IMEI、Android等设备识别

15、Activity的四种启动方式

standard
默认启动模式,当不指定Activity的启动模式,则使用这种启动方式启动Activity;这种启动方式每次都会创建新的实例,新创建的activity会置于栈顶。

singleTop
singleTop(栈顶复用模式),如果指定启动模式为singleTop模式,那么在启动时,系统会判断当前栈顶的Activity是不是要启动的Activity,如果是则不创建新的Activity而直接复用这个Activity,同时回掉Activity的onNewIntent方法;如果不是,则创建新的Activity,即使栈内已存在该Activity。

singleTask
栈内复用模式:但一个具有singleTask启动模式的Activity请求启动后,比如Activity A,系统首先会寻找是否存在A想要的任务栈,如果不存在,就重新创建一个任务栈,并将A的实例放入栈中。如果存在A所需要的任务栈,这时候要看A是否在栈中有实例存在,如果栈中不存在该实例,创建该Activity的实例并放入占中,如果栈中存在该Activity,复用该Activity,同时将位于该Activity上面的Activity全部销毁。

singleInstance
这种启动模式和singleTask几乎一样,它也只允许系统中存在一个目标Activity,包括上面我们所说的SingleTask的一些特性singleInstance都有。singleInstance翻译过来是单例的意思:TA有两层含义:1.告诉系统,我是独一无二的,2.告诉任务栈我是独一无二的,也就是说,任务栈中除了我不能再有其他Activity。

所以,如果要启动singleInstance模式的Activity,那只能新创建一个任务栈用来放它,因为人家说了,“我是独一无二的!”。同样的,如果从这种启动模式的Activity中启动别的Activity,那不好意思,我不管你是不是和我处在同一个应用,我所在的任务栈只能拥有我一个人,您呐,另外让系统给你创建一个任务栈待着去吧。

参考文章

三、算法部分

一、冒泡排序。


public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        
        for (int i = 0; i < n - 1; ++i) {
            // 每次遍历都将最大值移动到数组末尾
            for (int j = 0; j < n - i - 1; ++j) {
                if (arr[j] > arr[j + 1]) {
                    // 交换相邻元素位置
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
    
    public static void main(String[] args) {
        int[] array = {64, 34, 25, 12, 22, 11, 90};
        
        System.out.println("原始数组:");
        printArray(array);
        
        bubbleSort(array);
        
        System.out.println("\n排序后的数组:");
        printArray(array);
    }
    
    private static void printArray(int[] arr) {
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

四、鸿蒙部分

一、api9和api11的区别是什么。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值