diff --git a/.gitignore b/.gitignore index 71986661..c647db94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /.project /.DS_Store -/.DS_Store +/.idea diff --git "a/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" new file mode 100644 index 00000000..bf052c89 --- /dev/null +++ "b/AdavancedPart/1.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\270\200).md" @@ -0,0 +1,731 @@ + +热修复实现(一) +=== + + +现在的热修复方案已经有很多了,例如`alibaba`的[dexposed](https://github.com/alibaba/dexposed)、[AndFix](https://github.com/alibaba/AndFix)以及`jasonross`的[Nuwa](https://github.com/jasonross/Nuwa)等等。原来没有仔细去分析过也没想写这篇文章,但是之前[InstantRun详解][1]这篇文章中介绍了`Android Studio Instant Run`的 +实现原理,这不就是活生生的一个热修复吗? 随心情久久不能平复,我们何不用这种方式来实现。 + + +方案有很多种,我就只说明下我想到的方式,也就是`Instant Run`的方式: +分拆到不同的`dex`中,然后通过`classloader`来进行加载。但是在之前[`InstantRun`详解](https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/InstantRun%E8%AF%A6%E8%A7%A3.md)中只说到会通过内部的`server`去判断该类是否有更新,如果有的话就去从新的`dex`中加载该类,否则就从旧的`dex`中加载,但这是如何实现的呢? 怎么去从不同的`dex`中选择最新的那个来进行加载。 + +讲到这里需要先介绍一下`ClassLoader`: + +< `A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.` + + +在一般情况下,应用程序不需要创建`ClassLoader`对象,而是使用当前环境已经存在的`ClassLoader`。因为`Java`的`Runtime`环境在初始化时,其内部会创建一个`ClassLoader`对象用于加载`Runtime`所需的各种`Java`类。 +每个`ClassLoader`必须有一个父类,在装载`Class`文件时,子`ClassLoader`会先请求父`ClassLoader`加载该`Class`文件,只有当其父`ClassLoader`找不到该`Class`文件时,子`ClassLoader`才会继续装载该类,这是一种安全机制。 + +对于`Android`的应用程序,本质上虽然也是用`Java`开发,并且使用标准的`Java`编译器编译出`Class`文件,但最终的`APK`文件中包含的却是`dex`类型的文件。`dex`文件是将所需的所有`Class`文件重新打包,打包的规则不是简单的压缩,而是完全对`Class`文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是`dex`文件。由于`dex`文件是一种经过优化的`Class`文件,因此要加载这样特殊的`Class`文件就需要特殊的类装载器,这就是`DexClassLoader`,`Android SDK`中提供的D`exClassLoader`类就是出于这个目的。 + + +总体来说,`Android` 默认主要有三个`ClassLoader`: + +- `BootClassLoader`: 系统启动时创建 + `Provides an explicit representation of the boot class loader. It sits at the + head of the class loader chain and delegates requests to the VM's internal + class loading mechanism.` +- `PathClassLoader`: 可以加载`/data/app`目录下的`apk`,这也意味着,它只能加载已经安装的`apk`; +- `DexClassLoader`: 可以加载文件系统上的`jar`、`dex`、`apk`;可以从`SD`卡中加载未安装的`apk` + + +通过上面的分析知道,如果用多个`dex`的话肯定会用到`DexClassLoader`类,我们首先来看一下它的源码(这里 +插一嘴,源码可以去[googlesource](https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/dalvik/src/main/java/dalvik/system)中找): +```java +/** + * A class loader that loads classes from {@code .jar} and {@code .apk} files + * containing a {@code classes.dex} entry. This can be used to execute code not + * installed as part of an application. + * + *

This class loader requires an application-private, writable directory to + * cache optimized classes. Use {@code Context.getDir(String, int)} to create + * such a directory:

   {@code
+ *   File dexOutputDir = context.getDir("dex", 0);
+ * }
+ * + *

Do not cache optimized classes on external storage. + * External storage does not provide access controls necessary to protect your + * application from code injection attacks. + */ +public class DexClassLoader extends BaseDexClassLoader { + /** + * Creates a {@code DexClassLoader} that finds interpreted and native + * code. Interpreted classes are found in a set of DEX files contained + * in Jar or APK files. + * + *

The path lists are separated using the character specified by the + * {@code path.separator} system property, which defaults to {@code :}. + * + * @param dexPath the list of jar/apk files containing classes and + * resources, delimited by {@code File.pathSeparator}, which + * defaults to {@code ":"} on Android + * @param optimizedDirectory directory where optimized dex files + * should be written; must not be {@code null} + * @param libraryPath the list of directories containing native + * libraries, delimited by {@code File.pathSeparator}; may be + * {@code null} + * @param parent the parent class loader + */ + public DexClassLoader(String dexPath, String optimizedDirectory, + String libraryPath, ClassLoader parent) { + super(dexPath, new File(optimizedDirectory), libraryPath, parent); + } +} +``` +注释说的太明白了,这里就不翻译了,但是我们并没有找到加载的代码,去它的父类中查找, +因为家在都是从`loadClass()`方法中,所以我们去`ClassLoader`类中看一下`loadClass()`方法: +```java +/** + * Loads the class with the specified name. Invoking this method is + * equivalent to calling {@code loadClass(className, false)}. + *

+ * Note: In the Android reference implementation, the + * second parameter of {@link #loadClass(String, boolean)} is ignored + * anyway. + *

+ * + * @return the {@code Class} object. + * @param className + * the name of the class to look for. + * @throws ClassNotFoundException + * if the class can not be found. + */ + public Class loadClass(String className) throws ClassNotFoundException { + return loadClass(className, false); + } + + /** + * Loads the class with the specified name, optionally linking it after + * loading. The following steps are performed: + *
    + *
  1. Call {@link #findLoadedClass(String)} to determine if the requested + * class has already been loaded.
  2. + *
  3. If the class has not yet been loaded: Invoke this method on the + * parent class loader.
  4. + *
  5. If the class has still not been loaded: Call + * {@link #findClass(String)} to find the class.
  6. + *
+ *

+ * Note: In the Android reference implementation, the + * {@code resolve} parameter is ignored; classes are never linked. + *

+ * + * @return the {@code Class} object. + * @param className + * the name of the class to look for. + * @param resolve + * Indicates if the class should be resolved after loading. This + * parameter is ignored on the Android reference implementation; + * classes are not resolved. + * @throws ClassNotFoundException + * if the class can not be found. + */ + protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException { + Class clazz = findLoadedClass(className); + + if (clazz == null) { + ClassNotFoundException suppressed = null; + try { + // 先检查父ClassLoader是否已经家在过该类 + clazz = parent.loadClass(className, false); + } catch (ClassNotFoundException e) { + suppressed = e; + } + + if (clazz == null) { + try { + // 调用DexClassLoader.findClass()方法。 + clazz = findClass(className); + } catch (ClassNotFoundException e) { + e.addSuppressed(suppressed); + throw e; + } + } + } + + return clazz; + } +``` +上面会调用`DexClassLoader.findClass()`方法,但是`DexClassLoader`没有实现该方法,所以去它的父类`BaseDexClassLoader`中看,接着看一下`BaseDexClassLoader`的源码: +```java +/** + * Base class for common functionality between various dex-based + * {@link ClassLoader} implementations. + */ +public class BaseDexClassLoader extends ClassLoader { + /** originally specified path (just used for {@code toString()}) */ + private final String originalPath; + /** structured lists of path elements */ + private final DexPathList pathList; + /** + * Constructs an instance. + * + * @param dexPath the list of jar/apk files containing classes and + * resources, delimited by {@code File.pathSeparator}, which + * defaults to {@code ":"} on Android + * @param optimizedDirectory directory where optimized dex files + * should be written; may be {@code null} + * @param libraryPath the list of directories containing native + * libraries, delimited by {@code File.pathSeparator}; may be + * {@code null} + * @param parent the parent class loader + */ + public BaseDexClassLoader(String dexPath, File optimizedDirectory, + String libraryPath, ClassLoader parent) { + super(parent); + this.originalPath = dexPath; + this.pathList = + new DexPathList(this, dexPath, libraryPath, optimizedDirectory); + } + @Override + protected Class findClass(String name) throws ClassNotFoundException { + // 从DexPathList中找 + Class clazz = pathList.findClass(name); + if (clazz == null) { + throw new ClassNotFoundException(name); + } + return clazz; + } + @Override + protected URL findResource(String name) { + return pathList.findResource(name); + } + @Override + protected Enumeration findResources(String name) { + return pathList.findResources(name); + } + @Override + public String findLibrary(String name) { + return pathList.findLibrary(name); + } + +``` +在`findClass()`方法中我们看到调用了`DexPathList.findClass()`方法: +```java +/** + * A pair of lists of entries, associated with a {@code ClassLoader}. + * One of the lists is a dex/resource path — typically referred + * to as a "class path" — list, and the other names directories + * containing native code libraries. Class path entries may be any of: + * a {@code .jar} or {@code .zip} file containing an optional + * top-level {@code classes.dex} file as well as arbitrary resources, + * or a plain {@code .dex} file (with no possibility of associated + * resources). + * + *

This class also contains methods to use these lists to look up + * classes and resources.

+ */ +/*package*/ final class DexPathList { + private static final String DEX_SUFFIX = ".dex"; + private static final String JAR_SUFFIX = ".jar"; + private static final String ZIP_SUFFIX = ".zip"; + private static final String APK_SUFFIX = ".apk"; + /** class definition context */ + private final ClassLoader definingContext; + /** list of dex/resource (class path) elements */ + // 把dex封装成一个数组,每个Element代表一个dex + private final Element[] dexElements; + /** list of native library directory elements */ + private final File[] nativeLibraryDirectories; + + // ..... + + /** + * Finds the named class in one of the dex files pointed at by + * this instance. This will find the one in the earliest listed + * path element. If the class is found but has not yet been + * defined, then this method will define it in the defining + * context that this instance was constructed with. + * + * @return the named class or {@code null} if the class is not + * found in any of the dex files + */ + public Class findClass(String name) { + for (Element element : dexElements) { + DexFile dex = element.dexFile; + // 遍历数组,拿到第一个就返回 + if (dex != null) { + Class clazz = dex.loadClassBinaryName(name, definingContext); + if (clazz != null) { + return clazz; + } + } + } + return null; + } +} +``` +从上面的源码中分析,我知道系统会把所有相关的`dex`维护到一个数组中,然后在加载类的时候会从该数组中的第一个元素中取,然后返回。那我们只要保证将我们热修复后的`dex`对应的`Element`放到该数组的第一个位置就可以了,这样系统就会加载我们热修复的`dex`中的类。 +所以方案出来了,只要把有问题的类修复后,放到一个单独的`dex`,然后把该`Dex`转换成对应的`Element`后再将该`Element`插入到`dexElements`数组的第一个位置就可以了。那该如何去将其插入到`dexElements`数组的第一个位置呢?-- 暴力反射。 + + + +到这里我感觉初步的思路已经有了: + +- 将补丁作为`dex`发布。 +- 通过反射修改该`dex`所对应的`Element`在数组中的位置。 + +但是我也想到肯定还会有类似下面的问题: + +- 资源文件的处理 +- 四大组件的处理 +- 清单文件的处理 + + +虽然我知道没有这么简单,但是我还是决定抱着不作不死的宗旨继续前行。 + +好了,`demo`走起来。 + + +怎么生成`dex`文件呢? 这要讲过两部分: + +- `.class`-> `.jar` : `jar -cvf test.jar com/charon/instantfix_sample/MainActivity.class` +- `.jar`-> `.dex`: `dx --dex --output=target.jar test.jar` `target.jar`就是包含`.dex`的`jar`包 + + +生成好`dex`后我们为了模拟先将其放到`asset`目录下(实际开发中肯定要从接口中去下载,当然还会有一些版本号的判断等),然后就是将该`dex`转换成 + + +方案中采用的是`MultiDex`,对其进行一部分改造,具体代码: + +- 添加`dex`文件,并执行`install` + +```java +/** +* 添加apk包外的dex文件 +* 自动执行install +* @param dexFile +*/ +public static void addDexFileAutoInstall(Context context, List dexFile,File optimizedDirectory) { + if (dexFile != null && !dexFile.isEmpty() &&!dexFiles.contains(dexFile)) { + dexFiles.addAll(dexFile); + LogUtil.d(TAG, "add other dexfile"); + installDexFile(context,optimizedDirectory); + } +} +``` + +- `installDexFile`直接调用`MultiDex`的`installSecondaryDexes`方法 + +```java +/** + * 添加apk包外的dex文件, + * @param context + */ +publicstatic void installDexFile(Context context, File optimizedDirectory){ + if (checkValidZipFiles(dexFiles)) { + try { + installSecondaryDexes(context.getClassLoader(), optimizedDirectory, dexFiles); + } catch (IllegalAccessExceptione){ + e.printStackTrace(); + } catch (NoSuchFieldExceptione) { + e.printStackTrace(); + } catch (InvocationTargetExceptione){ + e.printStackTrace(); + } catch (NoSuchMethodExceptione) { + e.printStackTrace(); + } catch (IOExceptione) { + e.printStackTrace(); + } + } +} +``` + +- 将`patch.dex`放在所有`dex`最前面 + +```java +private static voidexpandFieldArray(Object instance, String fieldName, Object[]extraElements) throws NoSuchFieldException, IllegalArgumentException, +IllegalAccessException { + Field jlrField = findField(instance, fieldName); + Object[]original = (Object[]) jlrField.get(instance); + Object[]combined = (Object[]) Array.newInstance(original.getClass().getComponentType(),original.length + extraElements.length); + // 将后来的dex放在前面,主dex放在最后。 + System.arraycopy(extraElements, 0, combined, 0, extraElements.length); + System.arraycopy(original, 0, combined, extraElements.length,original.length); + // 原始的dex合并,是将主dex放在前面,其他的dex依次放在后面。 + //System.arraycopy(original, 0, combined, 0, original.length); + //System.arraycopy(extraElements, 0, combined, original.length,extraElements.length); + jlrField.set(instance, combined); +} +``` + +到此将`patch.dex`放进了`Element`,接下来的问题就是加载`Class`,当加载`patch.dex`中类的时候,会遇到一个问题,这个问题就是`QQ`空间团队遇到的`Class`的`CLASS_ISPREVERIFIED`。具体原因是`dvmResolveClass`这个方法对`Class`进行了校验。判断这个要`Resolve`的`class`是否和其引用来自一个`dex`。如果不是,就会遇到问题。 + +当引用这和被引用者不在同一个`dex`中就会抛出异常,导致`Resolve`失败。`QQ`空间团队的方案是阻止所有的`Class`类打上`CLASS_ISPREVERIFIED`来逃过校验,这种方式其实是影响性能。 +我们的方案是和`QQ`团队的类似,但是和`QQ`空间不同的是,我们将`fromUnverifiedConstant`设置为`true`,来逃过校验,达到补丁的路径。具体怎么实现呢? +要引用`Cydia Hook`技术来`hook Native dalvik`中`dvmResolveClass`这个方法。有关`Cydia Hook`技术请参考: + +- [官网地址](http://www.cydiasubstrate.com/) +- [官方教程](http://www.cydiasubstrate.com/id/38be592b-bda7-4dd2-b049-cec44ef7a73b) +- [SDK下载地址](http://asdk.cydiasubstrate.com/zips/cydia_substrate-r2.zip) + + +具体代码如下: +```java +//指明要hook的lib : +MSConfig(MSFilterLibrary,"/system/lib/libdvm.so") + +// 在初始化的时候进行hook +MSInitialize { + LOGD("Cydia Init"); + MSImageRef image; + //载入lib + image = MSGetImageByName("/system/lib/libdvm.so"); + if (image != NULL) { + LOGD("image is not null"); + void *dexload=MSFindSymbol(image,"dvmResolveClass"); + if(dexload != NULL) { + LOGD("dexloadis not null"); + MSHookFunction(dexload, (void*)proxyDvmResolveClass, (void**)&dvmResolveClass_Proxy); + } else{ + LOGD("errorfind dvmResolveClass"); + } + } +} +// 在初始化的时候进行hook//保留原来的地址 +ClassObject* (*dvmResolveClass_Proxy)(ClassObject* referrer, u4 classIdx, boolfromUnverifiedConstant); +// 新方法地址 +static ClassObject* proxyDvmResolveClass(ClassObject* referrer, u4 classIdx,bool fromUnverifiedConstant) { + return dvmResolveClass_Proxy(referrer, classIdx,true); +} +``` + +说到此处,似乎已经是一个完整的方案了,但在实践中,会发现运行加载类的时候报`preverified`错误,原来在`DexPrepare.cpp`,将`dex`转化成`odex`的过程中,会在`DexVerify.cpp`进行校验, +验证如果直接引用到的类和`clazz`是否在同一个`dex`,如果是,则会打上`CLASS_ISPREVERIFIED`标志。通过在所有类(`Application`除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的`dex`的类的引用,就可以解决这个问题。空间使用了`javaassist`进行编译时字节码插入。 + +所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上`CLASS_ISPREVERIFIED`标志。 最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下: + +```java +if (ClassVerifier.PREVENT_VERIFY) { + System.out.println(AntilazyLoad.class); +} +``` + +其中`AntilazyLoad`类会被打包成单独的`antilazy.dex`,这样当安装`apk`的时候,`classes.dex`内的类都会引用一个在不相同`dex`中的`AntilazyLoad`类,这样就防止了类被打上`CLASS_ISPREVERIFIED`的标志了,只要没被打上这个标志的类都可以进行打补丁操作。 然后在应用启动的时候加载进来`AntilazyLoad`类所在的`dex`包必须被先加载进来,不然`AntilazyLoad`类会被标记为不存在,即使后续加载了`hack.dex`包,那么他也是不存在的,这样屏幕就会出现茫茫多的类`AntilazyLoad`找不到的`log`。 所以`Application`作为应用的入口不能插入这段代码。(因为载入`hack.dex`的代码是在`Application`中`onCreate`中执行的,如果在`Application`的构造函数里面插入了这段代码,那么就是在`hack.dex`加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志) + +如何打包补丁包: + +1. 空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有`class`文件的`md5`,还有一份`mapping`混淆文件。 +2. 在后续的版本中使用`-applymapping`选项,应用正式版本的`mapping`文件,然后计算编译完成后的`class`文件的`md5`和正式版本进行比较,把不相同的`class`文件打包成补丁包。 +备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包`dex`,只需要把修改过的类的`class`文件打包成`patch dex`,然后放到`sdcard`下,那么就会让改变的代码生效。 + +在`Java`中,只有当两个实例的类名、包名以及加载其的`ClassLoader`都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的`ClassLoader`不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。 +同一个`Class`=相同的`ClassName + PackageName + ClassLoader` +以上问题在采用动态加载功能的开发中容易出现,请注意。 + +通过上面的分析,我们知道使用`ClassLoader`动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的`Java`程序,在`Android`程序中使用动态加载主要有两个麻烦的问题: + +- `Android`中许多组件类(如`Activity、Service`等)是需要在`Manifest`文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作; +- `Res`资源是`Android`开发中经常用到的,而`Android`是把这些资源用对应的`R.id`注册好,运行时通过这些`ID`从`Resource`实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到`R.id`的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的`Resource`实例中保存的资源`ID`对不上; + +说到底,抛开虚拟机的差别不说,一个`Android`程序和标准的`Java`程序最大的区别就在于他们的上下文环境`(Context)`不同。`Android`中,这个环境可以给程序提供组件需要用到的功能,也可以提供一些主题、`Res`等资源,其实上面说到的两个问题都可以统一说是这个环境的问题,而现在的各种`Android`动态加载框架中,核心要解决的东西也正是“如何给外部的新类提供上下文环境”的问题。 + +`DexClassLoader`的使用方法一般有两种: + +- 从已安装的`apk`中读取`dex` +- 从`apk`文件中读取`dex` + +假如有两个`APK`,一个是宿主`APK`,叫作`HOST`,一个是插件`APK`,叫作`Plugin`。`Plugin`中有一个类叫`PluginClass`,代码如下: +```java +public class PluginClass { + public PluginClass() { + Log.d("JG","初始化PluginClass"); + } + + public int function(int a, int b){ + return a+b; + } +} +``` +现在如果想调用插件`APK`中`PluginClass`内的方法,应该怎么办? + +#### 从已安装的apk中读取dex + +先来看第一种方法,这种方法必须建一个`Activity`,在清单文件中配置`Action`. + +```java + + + + + +``` +然后在宿主`APK`中如下使用: +```java +/** + * 这种方式用于从已安装的apk中读取,必须要有一个activity,且需要配置ACTION + */ +private void useDexClassLoader(){ + //创建一个意图,用来找到指定的apk + Intent intent = new Intent("com.maplejaw.plugin"); + //获得包管理器 + PackageManager pm = getPackageManager(); + List resolveinfoes = pm.queryIntentActivities(intent, 0); + if(resolveinfoes.size()==0){ + return; + } + //获得指定的activity的信息 + ActivityInfo actInfo = resolveinfoes.get(0).activityInfo; + + //获得包名 + String packageName = actInfo.packageName; + //获得apk的目录或者jar的目录 + String apkPath = actInfo.applicationInfo.sourceDir; + //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己 + //目录下的文件 + String dexOutputDir = getApplicationInfo().dataDir; + + //native代码的目录 + String libPath = actInfo.applicationInfo.nativeLibraryDir; + + //创建类加载器,把dex加载到虚拟机中 + DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath, + this.getClass().getClassLoader()); + + //利用反射调用插件包内的类的方法 + + try { + Class clazz = calssLoader.loadClass(packageName+".PluginClass"); + + Object obj = clazz.newInstance(); + Class[] param = new Class[2]; + param[0] = Integer.TYPE; + param[1] = Integer.TYPE; + + Method method = clazz.getMethod("function", param); + + Integer ret = (Integer)method.invoke(obj, 12,34); + + Log.d("JG", "返回的调用结果为:" + ret); + + } catch (Exception e) { + e.printStackTrace(); + } + } +``` +我们安装完两个`APK`后,在宿主中就可以直接调用,调用示例如下。 + +```java +public void btnClick(View view){ + useDexClassLoader(); +} +``` + +#### 从apk文件中读取dex + +这种方法由于并不需要安装,所以不需要通过`Intent`从`activity`中解析信息。换言之,这种方法不需要创建`Activity`。无需配置清单文件。我们只需要打包一个`apk`,然后放到`SD`卡中即可。 +核心代码如下: + +```java +//apk路径 +String path=Environment.getExternalStorageDirectory().getAbsolutePath()+"/1.apk"; + +private void useDexClassLoader(String path){ + + File codeDir=getDir("dex", Context.MODE_PRIVATE); + + //创建类加载器,把dex加载到虚拟机中 + DexClassLoader calssLoader = new DexClassLoader(path, codeDir.getAbsolutePath(), null, + this.getClass().getClassLoader()); + + //利用反射调用插件包内的类的方法 + + try { + Class clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass"); + + Object obj = clazz.newInstance(); + Class[] param = new Class[2]; + param[0] = Integer.TYPE; + param[1] = Integer.TYPE; + + Method method = clazz.getMethod("function", param); + + Integer ret = (Integer)method.invoke(obj, 12,21); + + Log.d("JG", "返回的调用结果为: " + ret); + + } catch (Exception e) { + e.printStackTrace(); + } + +``` + + +动态加载的几个关键问题: + +- 资源访问:无法找到某某`id`所对应的资源 + 因为将`apk`加载到宿主程序中去执行,就无法通过宿主程序的`Context`去取到`apk`中的资源,比如图片、文本等,这是很好理解的,因为`apk`已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文, + 用别人的`Context`是无法得到自己的资源的 + + - 解决方案一:插件中的资源在宿主程序中也预置一份; + 缺点:增加了宿主`apk`的大小;在这种模式下,每次发布一个插件都需要将资源复制到宿主程序中,这意味着每发布一个插件都要更新一下宿主程序; + - 解决方案二:将插件中的资源解压出来,然后通过文件流去读取资源; + 缺点:实际操作起来还是有很大难度的。首先不同资源有不同的文件流格式,比如图片、XML等,其次针对不同设备加载的资源可能是不一样的,如何选择合适的资源也是一个需要解决的问题; + 实际解决方案: + `Activity`中有一个叫`mBase`的成员变量,它的类型就是`ContextImpl`。注意到`Context`中有如下两个抽象方法,看起来是和资源有关的,实际上`Context`就是通过它们来获取资源的。这两个抽象方法的真正实现在`ContextImpl`中; + + +```java +/** Return an AssetManager instance for your application's package. */ + public abstract AssetManager getAssets(); + + /** Return a Resources instance for your application's package. */ + public abstract Resources getResources(); +``` +具体实现: +```java +protected void loadResources() { + try { + AssetManager assetManager = AssetManager.class.newInstance(); + Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); + addAssetPath.invoke(assetManager, mDexPath); + mAssetManager = assetManager; + } catch (Exception e) { + e.printStackTrace(); + } + Resources superRes = super.getResources(); + mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), + superRes.getConfiguration()); + mTheme = mResources.newTheme(); + mTheme.setTo(super.getTheme()); +} + +``` + +加载资源的方法是通过反射,通过调用`AssetManager`中的`addAssetPath`方法,我们可以将一个`apk`中的资源加载到`Resources`对象中,由于`addAssetPath`是隐藏`API`我们无法直接调用,所以只能通过反射。 + +```java +@hide + public final int addAssetPath(String path) { + synchronized (this) { + int res = addAssetPathNative(path); + makeStringBlocks(mStringBlocks); + return res; + } +} +``` + +`Activity`生命周期的管理:反射方式和接口方式。 +反射的方式很好理解,首先通过`Java`的反射去获取`Activity`的各种生命周期方法,比如`onCreate`、`onStart`、`onResume`等,然后在代理`Activity`中去调用插件`Activity`对应的生命周期方法即可: +缺点:一方面是反射代码写起来比较复杂,另一方面是过多使用反射会有一定的性能开销。 + +反射方式 +```java +@Override +protected void onResume() { + super.onResume(); + Method onResume = mActivityLifecircleMethods.get("onResume"); + if (onResume != null) { + try { + onResume.invoke(mRemoteActivity, new Object[] { }); + } catch (Exception e) { + e.printStackTrace(); + } + } +} + +@Override +protected void onPause() { + Method onPause = mActivityLifecircleMethods.get("onPause"); + if (onPause != null) { + try { + onPause.invoke(mRemoteActivity, new Object[] { }); + } catch (Exception e) { + e.printStackTrace(); + } + } + super.onPause(); +} +``` + +接口方式 + +```java +public interface DLPlugin { + + public void onStart(); + + public void onRestart(); + + public void onActivityResult(int requestCode, int resultCode, Intent + + data); + + public void onResume(); + + public void onPause(); + + public void onStop(); + + public void onDestroy(); + + public void onCreate(Bundle savedInstanceState); + + public void setProxy(Activity proxyActivity, String dexPath); + + public void onSaveInstanceState(Bundle outState); + + public void onNewIntent(Intent intent); + + public void onRestoreInstanceState(Bundle savedInstanceState); + + public boolean onTouchEvent(MotionEvent event); + + public boolean onKeyUp(int keyCode, KeyEvent event); + + public void onWindowAttributesChanged(LayoutParams params); + + public void onWindowFocusChanged(boolean hasFocus); + + public void onBackPressed(); + +… + +} +``` +代理`Activity`中只需要按如下方式即可调用插件`Activity`的生命周期方法,这就完成了插件`Activity`的生命周期的管理;插件`Activity`需要实现`DLPlugin`接口: +```java +@Override +protected void onStart() { + mRemoteActivity.onStart(); + super.onStart(); +} + +@Override +protected void onRestart() { + mRemoteActivity.onRestart(); + super.onRestart(); +} + +@Override +protected void onResume() { + mRemoteActivity.onResume(); + super.onResume(); +} +``` + + + +- [Google Instant app](https://developer.android.com/topic/instant-apps/index.html) +- [微信热补丁实现](https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF.md#rd) +- [多dex分拆](http://my.oschina.net/853294317/blog/308583) +- [QQ空间热修复方案](https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a) +- [Android dex分包方案](http://my.oschina.net/853294317/blog/308583) +- [类加载器DexClassLoader](http://www.maplejaw.com/2016/05/24/Android%E6%8F%92%E4%BB%B6%E5%8C%96%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%80%EF%BC%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8DexClassLoader/) +- [基于cydia Hook在线热修复补丁方案](http://blog.csdn.net/xwl198937/article/details/49801975) +- [Android 热补丁动态修复框架小结](http://blog.csdn.net/lmj623565791/article/details/49883661) +- [美团Android DEX自动拆包及动态加载简介](http://tech.meituan.com/mt-android-auto-split-dex.html) +- [插件化从放弃到捡起](http://kymjs.com/column/plugin.html) +- [无需Root也能使用Xposed!](http://weishu.me/) +- [当你准备开发一个热修复框架的时候,你需要了解的一切](http://www.zjutkz.net/2016/05/23/%E5%BD%93%E4%BD%A0%E5%87%86%E5%A4%87%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E7%83%AD%E4%BF%AE%E5%A4%8D%E6%A1%86%E6%9E%B6%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E4%B8%80%E5%88%87/) + + +[1]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/InstantRun%E8%AF%A6%E8%A7%A3.md "InstantRun详解" + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" "b/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" new file mode 100644 index 00000000..7d02b934 --- /dev/null +++ "b/AdavancedPart/2.\347\203\255\344\277\256\345\244\215\345\256\236\347\216\260(\344\272\214).md" @@ -0,0 +1,97 @@ + +热修复实现(二) +=== + +之前也分析过`InstantRun`的源码,前面也写了一篇热修复实现原理的文章。 + +But,最近遇到困难了,所在项目要做插件化,同事在开发过程中遇到了一个在5.0以下手机崩溃的问题,想着一起找找原因修复下。 +但是这个bug已经折腾了我两天了,只找到原因,并没有找出任何解决方案。 + +深深的感觉到之前的那些都是皮毛,没有真正的去做,真正的去处理一些细节,那些都是没任何意义的。 + +所以这里系统学习一下,既然学习,就找一个做的最好的来学。 + + +前面的文章也介绍了,目前存在的几个开源框架: + +- 手机淘宝基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术-Dexposed。 +但这个方案犹豫底层Dalvik结构过于依赖,最终无法兼容Android 5.0以后的ART虚拟机。 + +- 支付宝提出了新的方案Andfix,它同样是一种底层结构替换的方案,也达到了运行时生效即时修复的效果,而且做到了Dalvik和ART全版本的兼容。然而它也 +是由局限性的,且不说其底层固定结构的替换方案稳定性不好,其使用范文也存在着诸多限制,虽然可以通过代码改造来绕过限制达到同样的 +修复目的,但是这种方式即不优雅也不方便。而且更大的问题是Andfix只提供了代码层面的修复,对于资源和so的修复都还未实现。 + +- 其他的就是微信Tinker、饿了么的Amigo、美团的Robust,不过他们都各自有各自的局限性,或者不够稳定、或者补丁过大、或者效率低下 +,或者使用起来太繁琐,大部分技术上看起来似乎可行,但是实际体验并不好。 + +我们学习的就是阿里巴巴的新一代非侵入式Android热修复方案-Sophix。 +它各个方面都比较优秀,使用也比较方便,唯一不支持的就是四大组件的修复。这是因为如果修复四大组件,必须在AndroidManifest里面预先插入代码组件,并且尽可能声明所有权限,这样就会给 +原先的app添加很多臃肿的代码,对app运行流程的侵入性很强。 + + +在Sophix中,唯一需要的就是初始化和请求补丁两行代码,甚至连入口Application类我们都不需要做任何修改。 + +这样就给了开发者最大的透明度和自由度。我们甚至重新开发了打包工具,使的补丁工具操作图形界面化,这种所见即所得的补丁生成 +方式也是阿里热修复独家的,因此,Sophix的接入成本也是目前市面上所有方案里最低的。 + + + + +代码修复 +--- + +代码修复有两大主要方案: + +- 阿里系的底层替换方案:底层替换方案限制颇多,但是时效性最好,加载轻快,立即见效。 + + 底层替换方案是在已经加载了的类中直接替换掉原有的方法,是在原来类的基础上进行修改的。因而无法实现对原有类进行方法和字段的增减,因为这样将破坏原有类的结构。 + 一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个dex的方法数的变化,方法数的变化伴随着方法索引的变化,这样在 + 访问方法时就无法正常的索引到正确的方法了。如果字段发生了增加或减少,和方法变化的情况一样,所有字段的索引都会发生变化。 + 而新方法中使用到这些老的示例对象时,访问新增字段就会会产生不可预期的结果。 + + 这是该类方案的固有限制,而底层替换方案最为人逅病的地方,在于底层替换的不稳定性。因为Hook方案,都是直接依赖修改虚拟机方法实体的具体字段。因为Art虚拟机和Dalvik虚拟机的不同,每个版本的虚拟机都要适配,而且Android系统是开源的,各个厂商都可以对代码进行改造,如果某个厂商进行了修改,那么这种通用性的替换机制就会出问题。这便是不稳定的根源。 + + + +- 腾讯系的类加载方案:类加载方案时效性差,需要重新冷启动才能见效,但修复范围广、限制少。 + +类加载方案的原理是在app重新启动后让classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类,因此只有在下次重启的时候, +在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类的时候,就会用新类,从而达到热修复的目的。 + + +为什么sophix更好? +--- + +既然底层替换方案和类加载方案各有优点,把他们联合起来就是最好的选择。Sophix就是同时涵盖了这两种方案,可以实现优势互补、完全兼顾的作用,可以灵活的根据实际情况自动切换。 + + + + + + +但是[Sophix](https://help.aliyun.com/document_detail/57064.html?spm=a2c4g.11186623.6.543.SPhMhO)有一个缺点就是,他不是开源的,而且是收费的。但是确实强大。 + + + + +- [Google Instant app](https://developer.android.com/topic/instant-apps/index.html) +- [微信热补丁实现](https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF.md#rd) +- [多dex分拆](http://my.oschina.net/853294317/blog/308583) +- [QQ空间热修复方案](https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a) +- [Android dex分包方案](http://my.oschina.net/853294317/blog/308583) +- [类加载器DexClassLoader](http://www.maplejaw.com/2016/05/24/Android%E6%8F%92%E4%BB%B6%E5%8C%96%E6%8E%A2%E7%B4%A2%EF%BC%88%E4%B8%80%EF%BC%89%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8DexClassLoader/) +- [基于cydia Hook在线热修复补丁方案](http://blog.csdn.net/xwl198937/article/details/49801975) +- [Android 热补丁动态修复框架小结](http://blog.csdn.net/lmj623565791/article/details/49883661) +- [美团Android DEX自动拆包及动态加载简介](http://tech.meituan.com/mt-android-auto-split-dex.html) +- [插件化从放弃到捡起](http://kymjs.com/column/plugin.html) +- [无需Root也能使用Xposed!](http://weishu.me/) +- [当你准备开发一个热修复框架的时候,你需要了解的一切](http://www.zjutkz.net/2016/05/23/%E5%BD%93%E4%BD%A0%E5%87%86%E5%A4%87%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E7%83%AD%E4%BF%AE%E5%A4%8D%E6%A1%86%E6%9E%B6%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E4%BD%A0%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E4%B8%80%E5%88%87/) + + +[1]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/InstantRun%E8%AF%A6%E8%A7%A3.md "InstantRun详解" + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" "b/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" new file mode 100644 index 00000000..4da9dd5c --- /dev/null +++ "b/AdavancedPart/3.\347\203\255\344\277\256\345\244\215_addAssetPath\344\270\215\345\220\214\347\211\210\346\234\254\345\214\272\345\210\253\345\216\237\345\233\240(\344\270\211).md" @@ -0,0 +1,395 @@ +在做热修复功能时Java层通过反射调用addAssetPath在Android5.0及以上系统没有问题,在Android 4.x版本找不到资源。 + +addAssetPath方法: +```java +/** + * Add an additional set of assets to the asset manager. This can be + * either a directory or ZIP file. Not for use by applications. Returns + * the cookie of the added asset, or 0 on failure. + * {@hide} + */ +public final int addAssetPath(String path) { + int res = addAssetPathNative(path); + return res; +} +private native final int addAssetPathNative(String path); +``` + +addAssetPath具体的实现方法是native层的。 + +[AssetManager.cpp源码](https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1.0.1/libs/androidfw/AssetManager.cpp) + +4.4及以前的版本调用addAssetPath方法时,只是把补丁包的路径添加到mAssetPath中,不会去重新解析,真正解析的代码是在app第一次执行AssetManager.getResTable()`方法的时候。 +一旦解析完一次后,mResource对象就不为nil,以后就会直接return掉,不会重新解析。 + +```c +bool AssetManager::addAssetPath(const String8& path, void** cookie) +{ + AutoMutex _l(mLock); + asset_path ap; + String8 realPath(path); + if (kAppZipName) { + realPath.appendPath(kAppZipName); + } + ap.type = ::getFileType(realPath.string()); + if (ap.type == kFileTypeRegular) { + ap.path = realPath; + } else { + ap.path = path; + ap.type = ::getFileType(path.string()); + if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { + ALOGW("Asset path %s is neither a directory nor file (type=%d).", + path.string(), (int)ap.type); + return false; + } + } + // Skip if we have it already. + for (size_t i=0; i(this)->loadFileNameCacheLocked(); + const size_t N = mAssetPaths.size(); + // 真正解析package的地方 + for (size_t i=0; i(this)-> + mZipSet.getZipResourceTable(ap.path); + } + if (sharedRes == NULL) { + ass = const_cast(this)-> + mZipSet.getZipResourceTableAsset(ap.path); + if (ass == NULL) { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + if (ass != NULL && ass != kExcludedAsset) { + ass = const_cast(this)-> + mZipSet.setZipResourceTableAsset(ap.path, ass); + } + } + + if (i == 0 && ass != NULL) { + // If this is the first resource table in the asset + // manager, then we are going to cache it so that we + // can quickly copy it out for others. + ALOGV("Creating shared resources for %s", ap.path.string()); + sharedRes = new ResTable(); + sharedRes->add(ass, (void*)(i+1), false, idmap); + sharedRes = const_cast(this)-> + mZipSet.setZipResourceTable(ap.path, sharedRes); + } + } + } else { + ALOGV("loading resource table %s\n", ap.path.string()); + Asset* ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + shared = false; + } + if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { + if (rt == NULL) { + mResources = rt = new ResTable(); + updateResourceParamsLocked(); + } + ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); + if (sharedRes != NULL) { + ALOGV("Copying existing resources for %s", ap.path.string()); + rt->add(sharedRes); + } else { + ALOGV("Parsing resources for %s", ap.path.string()); + rt->add(ass, (void*)(i+1), !shared, idmap); + } + if (!shared) { + delete ass; + } + } + if (idmap != NULL) { + delete idmap; + } + MY_TRACE_END(); + } + if (required && !rt) ALOGW("Unable to find resources file resources.arsc"); + if (!rt) { + mResources = rt = new ResTable(); + } + return rt; +} + + +const ResTable& AssetManager::getResources(bool required) const +{ + const ResTable* rt = getResTable(required); + return *rt; +} +``` +而当我们执行加载补丁的代码的时候,getResTable已经执行过多次了,Android Framework里面的代码会多次调用该方法。所以即使是使用addAssetPath,也只是添加到了mAssetPath,并不会发生解析,所以补丁包里面的资源就是完全不生效的。 + +而在android 5.0及以上的代码中: + +[Android 5.0 AssetManager.cpp源码](https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r7/libs/androidfw/AssetManager.cpp) + +```c +bool AssetManager::addAssetPath(const String8& path, int32_t* cookie) +{ + AutoMutex _l(mLock); + asset_path ap; + String8 realPath(path); + if (kAppZipName) { + realPath.appendPath(kAppZipName); + } + ap.type = ::getFileType(realPath.string()); + if (ap.type == kFileTypeRegular) { + ap.path = realPath; + } else { + ap.path = path; + ap.type = ::getFileType(path.string()); + if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { + ALOGW("Asset path %s is neither a directory nor file (type=%d).", + path.string(), (int)ap.type); + return false; + } + } + // Skip if we have it already. + for (size_t i=0; i(i+1); + } + return true; + } + } + ALOGV("In %p Asset %s path: %s", this, + ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string()); + // Check that the path has an AndroidManifest.xml + Asset* manifestAsset = const_cast(this)->openNonAssetInPathLocked( + kAndroidManifest, Asset::ACCESS_BUFFER, ap); + if (manifestAsset == NULL) { + // This asset path does not contain any resources. + delete manifestAsset; + return false; + } + delete manifestAsset; + mAssetPaths.add(ap); + // new paths are always added at the end + if (cookie) { + *cookie = static_cast(mAssetPaths.size()); + } +#ifdef HAVE_ANDROID_OS + // Load overlays, if any + asset_path oap; + for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) { + mAssetPaths.add(oap); + } +#endif + if (mResources != NULL) { + // 重新调用该方法去解析package + appendPathToResTable(ap); + } + return true; +} +``` + +```c +bool AssetManager::appendPathToResTable(const asset_path& ap) const { + Asset* ass = NULL; + ResTable* sharedRes = NULL; + bool shared = true; + bool onlyEmptyResources = true; + MY_TRACE_BEGIN(ap.path.string()); + Asset* idmap = openIdmapLocked(ap); + size_t nextEntryIdx = mResources->getTableCount(); + ALOGV("Looking for resource asset in '%s'\n", ap.path.string()); + if (ap.type != kFileTypeDirectory) { + if (nextEntryIdx == 0) { + // The first item is typically the framework resources, + // which we want to avoid parsing every time. + sharedRes = const_cast(this)-> + mZipSet.getZipResourceTable(ap.path); + if (sharedRes != NULL) { + // skip ahead the number of system overlay packages preloaded + nextEntryIdx = sharedRes->getTableCount(); + } + } + if (sharedRes == NULL) { + ass = const_cast(this)-> + mZipSet.getZipResourceTableAsset(ap.path); + if (ass == NULL) { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + if (ass != NULL && ass != kExcludedAsset) { + ass = const_cast(this)-> + mZipSet.setZipResourceTableAsset(ap.path, ass); + } + } + + if (nextEntryIdx == 0 && ass != NULL) { + // If this is the first resource table in the asset + // manager, then we are going to cache it so that we + // can quickly copy it out for others. + ALOGV("Creating shared resources for %s", ap.path.string()); + sharedRes = new ResTable(); + sharedRes->add(ass, idmap, nextEntryIdx + 1, false); +#ifdef HAVE_ANDROID_OS + const char* data = getenv("ANDROID_DATA"); + LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set"); + String8 overlaysListPath(data); + overlaysListPath.appendPath(kResourceCache); + overlaysListPath.appendPath("overlays.list"); + addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx); +#endif + sharedRes = const_cast(this)-> + mZipSet.setZipResourceTable(ap.path, sharedRes); + } + } + } else { + ALOGV("loading resource table %s\n", ap.path.string()); + ass = const_cast(this)-> + openNonAssetInPathLocked("resources.arsc", + Asset::ACCESS_BUFFER, + ap); + shared = false; + } + if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { + ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); + if (sharedRes != NULL) { + ALOGV("Copying existing resources for %s", ap.path.string()); + mResources->add(sharedRes); + } else { + ALOGV("Parsing resources for %s", ap.path.string()); + mResources->add(ass, idmap, nextEntryIdx + 1, !shared); + } + onlyEmptyResources = false; + if (!shared) { + delete ass; + } + } else { + ALOGV("Installing empty resources in to table %p\n", mResources); + mResources->addEmpty(nextEntryIdx + 1); + } + if (idmap != NULL) { + delete idmap; + } + MY_TRACE_END(); + return onlyEmptyResources; +} +const ResTable* AssetManager::getResTable(bool required) const +{ + ResTable* rt = mResources; + if (rt) { + return rt; + } + // Iterate through all asset packages, collecting resources from each. + AutoMutex _l(mLock); + if (mResources != NULL) { + return mResources; + } + if (required) { + LOG_FATAL_IF(mAssetPaths.size() == 0, "No assets added to AssetManager"); + } + if (mCacheMode != CACHE_OFF && !mCacheValid) { + const_cast(this)->loadFileNameCacheLocked(); + } + mResources = new ResTable(); + updateResourceParamsLocked(); + bool onlyEmptyResources = true; + const size_t N = mAssetPaths.size(); + // 也是调用appendPathToResTable去解析 + for (size_t i=0; i = instance.getStatusById(workRequest.id) +instance.cancelWorkById(workRequest.id) +``` +如果想要顺序的去执行不同的`OneTimeWorker`也是很方便的: +```kotlin +WorkContinuation chain1 = WorkManager.getInstance() + .beginWith(workA) + .then(workB); +WorkContinuation chain2 = WorkManager.getInstance() + .beginWith(workC) + .then(workD); +WorkContinuation chain3 = WorkContinuation + .combine(chain1, chain2) + .then(workE); +chain3.enqueue(); +``` + +上面都是用了`OneTimeWorkRequest`,如果你想定期的去执行某一个`worker`的话,可以使用`PeriodicWorkRequest`: +```kotlin +val workRequest = PeriodicWorkRequest.Builder(MineWorker::class.java, 3, TimeUnit.SECONDS) + .setConstraints(constraints) + .setInputData(data) + .build() +``` + +但是我使用这个定期任务执行的时候也只是执行了一次,并没有像上面的代码中那样3秒执行一次,什么鬼? +我们看看`PeriodicWorkRequest`的源码: + +```kotlin +/** + * A class that represents a request for repeating work. + */ + +public final class PeriodicWorkRequest extends WorkRequest { + + /** + * The minimum interval duration for {@link PeriodicWorkRequest}, in milliseconds. + * Based on {@see https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/job/JobInfo.java#110}. + */ + public static final long MIN_PERIODIC_INTERVAL_MILLIS = 15 * 60 * 1000L; // 15 minutes. +``` + +上面说的很明白,最小间隔15分钟,- -! + + +总体来说,`WorkManager`并不是要取代线程池`AsyncTask/RxJava`.反而是像`AlarmManager`来做定时任务的意思.即保证你给它的任务能完成, 即使你的应用都没有被打开, 或是设备重启后也能让你的任务被执行.`WorkManager`在设计上设计得比较好.没有把`worker`,任务混为一谈,而是把它们解耦成`Worker`,`WorkRequest`.这样分层就清晰多了, 也好扩展. + + +到了这里突然有了一个大胆的想法。看到没有它能保证任务的执行。 +我们之前写过一篇文章[Android卸载反馈](https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Android%E5%8D%B8%E8%BD%BD%E5%8F%8D%E9%A6%88.md) +里面用到了`c`中的`fork`来保证存活,达到常驻内存的功能,如果`PeriodicWorkRequest`的最小间隔时间比较短不是15分钟的话,那这里是不是也可以用`WorkManager`来实现? 好了,不说了。 + + + +参考: + +- [官方文档](https://developer.android.com/topic/libraries/architecture/workmanager) +- [Codelabs android-workmanager](https://codelabs.developers.google.com/codelabs/android-workmanager/#0) +- [示例代码](https://github.com/googlecodelabs/android-workmanager) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" "b/AdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" new file mode 100644 index 00000000..d7f32de9 --- /dev/null +++ "b/AdavancedPart/Android6.0\346\235\203\351\231\220\347\263\273\347\273\237.md" @@ -0,0 +1,603 @@ +Android6.0权限系统 +=== + +`Android`权限系统是一个非常重要的安全问题,因为它只有在安装时会询问一次。一旦软件本安装之后,应用程序可以在用户毫不知情的情况下使用这些权限来获取所有的内容。 + +很多坏蛋会通过这个安全缺陷来收集用户的个人信息并使用它们来做坏事的情况就不足为奇了。 + +`Android`团队也意识到了这个问题。在经过了7年后,权限系统终于被重新设置了。从`Anroid 6.0(API Level 23)`开始,应用程序在安装时不会被授予任何权限,取而代之的是在运行时应用回去请求用户授予对应的权限。这样可以让用户能更好的去控制应用功能。例如,一个用户可能会同一个拍照应用使用摄像头的权限,但是不同授权它获取设备位置的权限。用户可以在任何时候通过去应用的设置页面来撤销授权。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/runtimepermission.jpg?raw=true) + +注意:上面请求权限的对话框不会自动弹出。开发者需要手动的调用。如果开发者调用了一些需要权限的功能,但是用户又拒绝授权的话,应用就会`crash`。 + +系统权限被分为两类,`normal`和`dangerous`: + +- `Normal Permissions`不需要用户直接授权,如果你的应用在清单文件中声明了`Normal Permissions`,系统会自动授权该权限。 +- `Dangerous Permissions`可以让应用获取用户的私人数据。如果你的应用在清单文件中申请了`Dangerous Permissions`,那就必须要用户来授权给应用。 + +`Normal Permissions`: +`Normal Permission`是当用户安装或更新应用时,系统将授予应用所请求的权限,又称为`PROTECTION_NORMAL`(安装时授权的一类基本权限)。该类权限只需要在`manifest`文件中声明即可,安装时就授权,不需要每次使用时进行检查,而且用户不能取消以上授权。 + + +- ACCESS_LOCATION_EXTRA_COMMANDS +- ACCESS_NETWORK_STATE +- ACCESS_NOTIFICATION_POLICY +- ACCESS_WIFI_STATE +- BLUETOOTH +- BLUETOOTH_ADMIN +- BROADCAST_STICKY +- CHANGE_NETWORK_STATE +- CHANGE_WIFI_MULTICAST_STATE +- CHANGE_WIFI_STATE +- DISABLE_KEYGUARD +- EXPAND_STATUS_BAR +- GET_PACKAGE_SIZE +- INSTALL_SHORTCUT +- INTERNET +- KILL_BACKGROUND_PROCESSES +- MODIFY_AUDIO_SETTINGS +- NFC +- READ_SYNC_SETTINGS +- READ_SYNC_STATS +- RECEIVE_BOOT_COMPLETED +- REORDER_TASKS +- REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +- REQUEST_INSTALL_PACKAGES +- SET_ALARM +- SET_TIME_ZONE +- SET_WALLPAPER +- SET_WALLPAPER_HINTS +- TRANSMIT_IR +- UNINSTALL_SHORTCUT +- USE_FINGERPRINT +- VIBRATE +- WAKE_LOCK +- WRITE_SYNC_SETTINGS + + + +下图为`Dangerous permissions and permission groups`: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/permgroup.png?raw=true) + + +在所有的`Android`版本中,你的应用都需要在`manifest`文件中声明`normal`和`dangerous`权限。然而声明所影响的效果会因系统版本和你应用的`target SDK lever`有关: + +- 如果设备运行的是`Android 5.1`或者之前的系统,或者应用的`targetSdkVersion`是22或者之前版本:如果你在`manifest`中声明了`dangerous permission`,用户需要在安装应用时授权这些权限。如果用户不授权这些权限,系统就不会安装该应用。(我试了下发现即使`targetSdkVersion`是22及以下,在6.0的手机上时,如果你安装时你不同意一些权限,也仍然可以安装的) +- 如果设备运行的是`Android 6.0`或者更高的系统,并且你应用的`targetSdkVersion`是23或者更高:应用必须在`manifest`文件中申请这些权限,而且必须要在运行时对所需要的`dangerous permission`申请授权。用户可以统一或者拒绝授权,并且及时用户拒绝了授权,应用在无法使用一些功能的情况下也要保证能继续运行。 + +也就是说新的运行时权限仅当我们设置`targetSdkVersion`是23及以上时才会起作用。 +如果你的`targtSdkVersion`低于23,那将被认为该应用没有经过`android 6.0`的测试,当该应用被安装到了6.0的手机上时,仍然会使用之前的旧权限规则,在安装时会提示所有需要的权限(这样做是有道理的,不然对于之前开发的应用,我们都要立马去修改让它适应6.0,来不及的话就导致6.0的手机都无法使用了,显然Android开发团队不会考虑不到这种情况),当然用户可以在安装的界面不允许一些权限,那当程序使用到了这些权限时,会崩溃吗?答案是在`Android 6.0`及以上的手机会直接`crash`,但是在`23`之前的手机上不会`crash`。 + +所以如果你的应用没有支持运行时权限的功能,那千万不要讲`targetSdkVersion`设置为23,否则就麻烦了。 + +> 注意:从`Android 6.0(API Level 23)`开始,即使应用的`targetSdkVersion`是比较低的版本,但是用户仍然可以在任何时候撤销对应用的授权。所以不管应用的`targetSdkVerison`是什么版本,你都要测试你的应用在不能获取权限时能不能正常运行。 + +下面介绍下如何使用`Android Support Library`来检查和请求权限。`Android`框架在`6.0`开始也提供了相同的方法。然而使用`support`包会比较简单,因为这样你就不需要在请求方法时判断当前的系统版本。(后面说的这几个类都是`android.support.v4`中的) + +### 检查权限 + +如果应用需要使用`dangerous permission`,在任何时候执行需要该权限的操作时你都需要检查是否已经授权。用户可能会经常取消授权,所以即使昨天应用已经使用了摄像头,这也不能保证今天仍然有使用摄像头的权限。 + +为了检查是否可以使用该权限,调用`ContextCompat.checkSelfPermission()`。 +例如: +```java +// Assume thisActivity is the current activity +int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, + Manifest.permission.WRITE_CALENDAR); +``` +如果应用有该权限,该方法将返回`PackageManager.PERMISSION_GRANTED`,应用可以进行相关的操作。如果应用不能使用该权限,该方法将返回`PERMISSION_DENIED`,这是应用将必须要向用户申请该权限。 + + +### 申请使用权限 + +如果应用需要使用清单文件中申明的`dangerous permission`,它必须要向用户申请来授权。`Android`提供了几种申请授权的方法。使用这些方法时将会弹出一个标准的系统对话框,该对话框不能自定义。 + +##### 说明为什么应用需要使用这些权限 + +在一些情况下,你可能需要帮助用力理解为什么需要该权限。例如,一个用户使用了一个照相的应用,用户不会奇怪为什么应用申请使用摄像头的权限,但是用户可能会不理解为什么应用需要获取位置或者联系人的权限。在请求一个权限之前,你需要该用户一个说明。一定要切记不要通过说明来压倒用户。如果你提供了太多的说明,用户可能会感觉沮丧并且会卸载它。 + +一个你需要提供说明的合适时机就是在用户之前已经不同意授权该权限的情况下。如果一个用户继续尝试使用需要权限的功能时,但是之前确禁止了该权限的请求,这就可能是因为用户不理解为什么该功能需要使用该权限。在这种情况下,提供一个说明是非常合适的。 + +为了能找到用户可能需要说明的情况,`android`提供了一个工具类方法`ActivityCompat.shouldShowRequestPermissionRationale().`。如果应用之前申请了该权限但是用户拒绝授权后该方法会返回`true`。(在Android 6.0之前调用的时候会直接返回false) + +> 注意:如果用户之前拒绝了权限申请并且选择了请求权限对话框中的`Don’t ask again`选项,该方法就会返回`false`。如果设备策略禁止了该应用使用该权限,该方法也会返回`false`。(我测试的时候发现请求权限的对话框中并没有`Don’t asdk again`这一项) + + + +##### 申请需要的权限 + +如果应用没有所需的权限时,应用必须调用`ActivityCompat.requestPermissions (Activity activity, + String[] permissions, + int requestCode)`方法来申请对用的权限。参数传递对应所需的权限以及一个整数型的`request code`来标记该权限申请。 该方法是异步的:该方法会立即返回,在用户响应了请求权限的对话框之后,系统会调用对用的回调方法来通知结果,并且会传递在`reqeustPermissions()`方法中的`request code`。(在Android 6.0之前调用的时候会直接去调用`onRequestPermissionsResult()`的回调方法) +如图: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/requestpermission.jpg?raw=true) + + +下面是检查是否读取联系人权限,并且在必要时申请权限的代码: + +```java +// Here, thisActivity is the current activity +if (ContextCompat.checkSelfPermission(thisActivity, + Manifest.permission.READ_CONTACTS) + != PackageManager.PERMISSION_GRANTED) { + + // Should we show an explanation? + if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, + Manifest.permission.READ_CONTACTS)) { + + // Show an expanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + + } else { + + // No explanation needed, we can request the permission. + + ActivityCompat.requestPermissions(thisActivity, + new String[]{Manifest.permission.READ_CONTACTS}, + MY_PERMISSIONS_REQUEST_READ_CONTACTS); + + // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an + // app-defined int constant. The callback method gets the + // result of the request. + } +} +``` + +> 注意:当调用`requestPermissions()`方法时,系统会显示一个标准的对话框。应用不能指定或者改变该对话框。如果你想提供一些信息或者说明给用户,你需要在调用`requestPermissions()`之前处理。 + +##### 处理请求权限的的结果 + +如果应用申请权限,系统会显示一个对话框。当用户相应后,系统会调用应用中的`onRequestPermissionsResult (int requestCode, + String[] permissions, + int[] grantResults)`方法并传递用户的操作结果。在应用中必须要重写该方法来查找授权了什么权限。该回调方法会传递你在`requestPermisssions()`方法中传递的`request code`。直接在`Activity`或者`Fragment`中重写`onRequestPermissionsResult()`方法即可。例如,申请`READ_CONTACTS`的权限可能会有下面的回到方法: + +```java +@Override +public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + case MY_PERMISSIONS_REQUEST_READ_CONTACTS: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + + // permission was granted, yay! Do the + // contacts-related task you need to do. + + } else { + + // permission denied, boo! Disable the + // functionality that depends on this permission. + } + return; + } + + // other 'case' lines to check for other + // permissions this app might request + } +} + +``` + +系统提示的对话框会描述应用所需的`permission groud`。它不会列出特定的权限。例如,如果你申请了`READ_CONTACTS`权限,系统的对话框只会说你的应用需要获取设备的联系人信息。用户只需要授权每个`permission group`一次。如果你应用需要申请其他任何一个在该`permission group`中的权限时,系统会自动授权。在申请这些授权时,系统会像用户明确通过系统对话框统一授权时一样去调用`onRequestPermissionsResult()`方法并且传递`PERMISSION_GRANTED`参数。 + +> 注意:虽然用户已经授权了同一`permission group`中其他的任何权限,但是应用仍然需要明确申请每个需要的权限。例外,`permission group`中的权限在以后可能会发生变化。 + +例如,假设在应用的`manifest`文件中同时声明了`READ_CONTACTS`和`WRITE_CONTACTS`权限。如果你申请`READ_CONTACTS`权限而且用户同意了该权限,如果你想继续申请`WRITE_CONTACTS`权限,系统不会与用户有任何交互就会直接进行授权。 + +如果用户拒绝了一个权限申请,你的应用进行合适的处理。例如,你的应用可能显示一个对话框来表明无法执行用户请求的需要该权限的操作。 + +如果系统向用户申请权限授权,用户选择了让系统以后不要再申请该权限。 在这种情况下,应用在任何时间调用`reqeustPermissions()`方法来再次申请权限时,系统都会直接拒绝该请求。系统会直接调用`onRequestPermissionResult()`回调方法并且传递`PERMISSION_DENIED`参数,和用户明确拒绝应用申请该权限时一样。 这就意味着在你调用`requestPermissions()`方法是,你无法确定是否会和用户有直接的交互操作。 + + +示例代码: +```java + +final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124; + +private void insertDummyContactWrapper() { + List permissionsNeeded = new ArrayList(); + + final List permissionsList = new ArrayList(); + if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION)) + permissionsNeeded.add("GPS"); + if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS)) + permissionsNeeded.add("Read Contacts"); + if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS)) + permissionsNeeded.add("Write Contacts"); + + if (permissionsList.size() > 0) { + if (permissionsNeeded.size() > 0) { + // Need Rationale + String message = "You need to grant access to " + permissionsNeeded.get(0); + for (int i = 1; i < permissionsNeeded.size(); i++) + message = message + ", " + permissionsNeeded.get(i); + showMessageOKCancel(message, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + requestPermissions(permissionsList.toArray(new String[permissionsList.size()]), + REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); + } + }); + return; + } + requestPermissions(permissionsList.toArray(new String[permissionsList.size()]), + REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); + return; + } + + insertDummyContact(); +} + +private boolean addPermission(List permissionsList, String permission) { + if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { + permissionsList.add(permission); + // Check for Rationale Option + if (!shouldShowRequestPermissionRationale(permission)) + return false; + } + return true; +} +``` + + +上面讲到的都是`Activity`中的使用方法,那`Fragment`中怎么授权呢? +如果在`Fragment`中使用,用`v13`包中的`FragmentCompat.requestPermissions()`和`FragmentCompat.shouldShowRequestPermissionRationale()`。 + + + +在`Fragment`中申请权限,不要使用`ActivityCompat.requestPermissions`, 直接使用`Fragment.requestPermissions`方法, +否则会回调到`Activity`的`onRequestPermissionsResult`。但是虽然你使用`Fragment.requestPermissions`方法,也照样回调不到`Fragment.onRequestPermissionsResult`中。这是`Android`的`Bug`,[详见](https://code.google.com/p/android/issues/detail?can=2&start=0&num=100&q=&colspec=ID%20Status%20Priority%20Owner%20Summary%20Stars%20Reporter%20Opened&groupby=&sort=&id=189121),`Google`已经在`23.3.0`修复了该问题,所以要尽快升级。 + +所以升级到`23.3.0`及以上就没问题了。如果不升级该怎么处理呢?就是在`Activity.onRequestPermissionsResult`方法中去手动调用每个`Fragment`的方法(当然你要判断下权限个数,不然申请一个权限的情况下会重复调用). +```java +@Override +public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + List fragments = getSupportFragmentManager().getFragments(); + if (fragments != null) { + for (Fragment fragment : fragments) { + fragment.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } +} +``` + +我简单的写了一个工具类: + +```java +public class PermissionUtil { + /** + * 在调用需要权限的功能时使用该方法进行检查。 + * + * @param activity + * @param requestCode + * @param iPermission + * @param permissions + */ + public static void checkPermissions(Activity activity, int requestCode, IPermission iPermission, String... permissions) { + handleRequestPermissions(activity, requestCode, iPermission, permissions); + } + + public static void checkPermissions(Fragment fragment, int requestCode, IPermission iPermission, String... permissions) { + handleRequestPermissions(fragment, requestCode, iPermission, permissions); + } + + public static void checkPermissions(android.app.Fragment fragment, int requestCode, IPermission iPermission, String... permissions) { + handleRequestPermissions(fragment, requestCode, iPermission, permissions); + } + + /** + * 在Actvitiy或者Fragment中重写onRequestPermissionsResult方法后调用该方法。 + * + * @param activity + * @param requestCode + * @param permissions + * @param grantResults + * @param iPermission + */ + public static void onRequestPermissionsResult(Activity activity, int requestCode, String[] permissions, + int[] grantResults, IPermission iPermission) { + requestResult(activity, requestCode, permissions, grantResults, iPermission); + + } + + public static void onRequestPermissionsResult(Fragment fragment, int requestCode, String[] permissions, + int[] grantResults, IPermission iPermission) { + requestResult(fragment, requestCode, permissions, grantResults, iPermission); + } + + public static void onRequestPermissionsResult(android.app.Fragment fragment, int requestCode, String[] permissions, + int[] grantResults, IPermission iPermission) { + requestResult(fragment, requestCode, permissions, grantResults, iPermission); + } + + public static void requestPermission(T t, int requestCode, String... permission) { + List permissions = new ArrayList<>(); + for (String s : permission) { + permissions.add(s); + } + requestPermissions(t, requestCode, permissions); + } + + /** + * 在检查权限后自己处理权限说明的逻辑后调用该方法,直接申请权限。 + * + * @param t + * @param requestCode + * @param permissions + * @param + */ + public static void requestPermission(T t, int requestCode, List permissions) { + if (permissions == null || permissions.size() == 0) { + return; + } + requestPermissions(t, requestCode, permissions); + } + + public static boolean checkSelfPermission(Context context, String permission) { + if (context == null || TextUtils.isEmpty(permission)) { + throw new IllegalArgumentException("invalidate params: the params is null !"); + } + + context = context.getApplicationContext(); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + int result = ContextCompat.checkSelfPermission(context, permission); + if (PackageManager.PERMISSION_DENIED == result) { + return false; + } + } + + return true; + } + + private static void handleRequestPermissions(T t, int requestCode, IPermission iPermission, String... permissions) { + if (t == null || permissions == null || permissions.length == 0) { + throw new IllegalArgumentException("invalidate params"); + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + Activity activity = getActivity(t); + List deniedPermissions = getDeniedPermissions(activity, permissions); + if (deniedPermissions != null && deniedPermissions.size() > 0) { + + List rationalPermissions = new ArrayList<>(); + for (String deniedPermission : deniedPermissions) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, + deniedPermission)) { + rationalPermissions.add(deniedPermission); + } + } + + boolean showRational = false; + if (iPermission != null) { + showRational = iPermission.showRational(requestCode); + } + + if (rationalPermissions.size() > 0 && showRational) { + if (iPermission != null) { + iPermission.onRational(requestCode, deniedPermissions); + } + } else { + requestPermissions(t, requestCode, deniedPermissions); + } + } else { + if (iPermission != null) { + iPermission.onGranted(requestCode); + } + } + } else { + if (iPermission != null) { + iPermission.onGranted(requestCode); + } + } + } + + @Nullable + private static Activity getActivity(T t) { + Activity activity = null; + if (t instanceof Activity) { + activity = (Activity) t; + } else if (t instanceof Fragment) { + activity = ((Fragment) t).getActivity(); + } else if (t instanceof android.app.Fragment) { + activity = ((android.app.Fragment) t).getActivity(); + } + return activity; + } + + @TargetApi(Build.VERSION_CODES.M) + private static void requestPermissions(T t, int requestCode, List deniedPermissions) { + if (deniedPermissions == null || deniedPermissions.size() == 0) { + return; + } + // has denied permissions + if (t instanceof Activity) { + ((Activity) t).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); + } else if (t instanceof Fragment) { + ((Fragment) t).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); + } else if (t instanceof android.app.Fragment) { + ((android.app.Fragment) t).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); + } + } + + private static List getDeniedPermissions(Context context, String... permissions) { + if (context == null || permissions == null || permissions.length == 0) { + return null; + } + List denyPermissions = new ArrayList<>(); + for (String permission : permissions) { + if (!checkSelfPermission(context, permission)) { + denyPermissions.add(permission); + } + } + return denyPermissions; + } + + + private static void requestResult(T t, int requestCode, String[] permissions, + int[] grantResults, IPermission iPermission) { + List deniedPermissions = new ArrayList<>(); + for (int i = 0; i < grantResults.length; i++) { + if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { + deniedPermissions.add(permissions[i]); + } + } + if (deniedPermissions.size() > 0) { + if (iPermission != null) { + iPermission.onDenied(requestCode); + } + } else { + if (iPermission != null) { + iPermission.onGranted(requestCode); + } + } + } + +} + +interface IPermission { + void onGranted(int requestCode); + + void onDenied(int requestCode); + + void onRational(int requestCode, List permissions); + + /** + * 是否需要提示用户该权限的作用,提示后需要再调用requestPermission()方法来申请。 + * + * @return true 为提示,false为不提示 + */ + boolean showRational(int requestCode); +} + +``` + +使用方法: +```java +public class MainFragment extends Fragment implements View.OnClickListener { + private Button mReqCameraBt; + private Button mReqContactsBt; + private Button mReqMoreBt; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_main, container, false); + findView(view); + initView(); + return view; + } + + private void findView(View view) { + mReqCameraBt = (Button) view.findViewById(R.id.bt_requestCamera); + mReqContactsBt = (Button) view.findViewById(R.id.bt_requestContacts); + mReqMoreBt = (Button) view.findViewById(R.id.bt_requestMore); + } + + private void initView() { + mReqCameraBt.setOnClickListener(this); + mReqContactsBt.setOnClickListener(this); + mReqMoreBt.setOnClickListener(this); + } + + + public static final int REQUEST_CODE_CAMERA = 0; + public static final int REQUEST_CODE_CONTACTS = 1; + public static final int REQUEST_CODE_MORE = 2; + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + PermissionUtil.onRequestPermissionsResult(this, requestCode, permissions, grantResults, mPermission); + } + + public void requestCamera() { + PermissionUtil.checkPermissions(this, REQUEST_CODE_CAMERA, mPermission, Manifest.permission.CAMERA); + } + + public void requestReadContacts() { + PermissionUtil.checkPermissions(this, REQUEST_CODE_CONTACTS, mPermission, Manifest.permission.READ_CONTACTS); + } + + public void requestMore() { + PermissionUtil.checkPermissions(this, REQUEST_CODE_MORE, mPermission, Manifest.permission.READ_CONTACTS, Manifest.permission.READ_CALENDAR, Manifest.permission.CALL_PHONE); + } + + private void showPermissionTipDialog(final int requestCode, final List permissions) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage("I want you permissions"); + builder.setTitle("Hello Permission"); + builder.setPositiveButton("确认", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + PermissionUtil.requestPermission(MainFragment.this, requestCode, permissions); + } + }); + builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + builder.create().show(); + } + + public IPermission mPermission = new IPermission() { + @Override + public void onGranted(int requestCode) { + Toast.makeText(getActivity(), "onGranted :" + requestCode, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onDenied(int requestCode) { + Toast.makeText(getActivity(), "onDenied :" + requestCode, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onRational(int requestCode, List permission) { + showPermissionTipDialog(requestCode, permission); + } + + @Override + public boolean showRational(int requestCode) { + switch (requestCode) { + case REQUEST_CODE_MORE: + return true; + + default: + break; + } + return false; + } + }; + + @Override + public void onClick(View v) { + int id = v.getId(); + switch (id) { + case R.id.bt_requestCamera: + requestCamera(); + break; + case R.id.bt_requestContacts: + requestReadContacts(); + break; + case R.id.bt_requestMore: + requestMore(); + break; + } + } +} + +``` + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md" "b/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md" new file mode 100644 index 00000000..7efca459 --- /dev/null +++ "b/AdavancedPart/AndroidRuntime_ART\344\270\216Dalvik.md" @@ -0,0 +1,161 @@ +AndroidRuntime_ART与Dalvik +=== +在说Android Runtime之前,我们需要了解什么是运行时环境,还需要了解一些基本知识,即JVM和Dalvik VM的功能。 + +## Runtime + +用最简单的术语来说,它是操作系统使用的系统,它负责将您用Java之类的高级语言编写的代码转换为CPU/处理器能理解机器代码。 + +运行时包含在程序运行时执行的软件指令,即使它们实际上并不是该软件代码的一部分也是如此。 +CPU或更笼统的术语我们的计算机仅理解机器语言(二进制代码),因此要使其在CPU上运行,必须将代码转换为机器代码,这由翻译器完成。 +因此,以下是按顺序生成翻译器的过程: +- Assemblers + 它可以直接将汇编代码转换为机器代码,因此速度非常快。 +- Compilers + 它将代码转换为汇编代码,然后使用汇编程序将代码转换为二进制。使用此编译速度很慢,但是执行速度很快。但是编译器的最大问题是所生成的机器代码取决于平台。换句话说,在一台计算机上运行的代码可能不会在另一台计算机上运行。 +- Interpreters + 它在执行代码时翻译代码。由于翻译是在运行时进行的,因此执行速度很慢。 +### JVM +为了维持代码的平台独立性,JAVA开发了JVM,即Java虚拟机。它针对每个平台开发了JVM,这意味着JVM依赖于该平台。Java编译器将.java文件转换为.class文件,这称为字节码。该字节码被提供给JVM,该JVM将其转换为机器码。 + +### Android Runtime + +当我们构建应用程序并生成APK时,该APK的一部分是.dex文件。这些文件包含我们应用程序的源代码,包括我们在为软件解释器设计的低级代码(字节码)中使用的所有库。 + +当用户运行我们的应用程序时,写入的.dex文件中的字节码将由Android Runtime转换为机器码—一组指令,机器可以直接理解并由CPU处理。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dex_local_code.png) + +Android Runtime同样也回管理内存及垃圾回收。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_dvm.png) + + + +可以使用各种策略将字节码编译为机器代码,所有这些策略都有其取舍。为了了解Android Runtime的工作原理,需要首先了解Dalvik。 + + + +## Android Runtime的发展 + +### Dalvik(<= Android K) + +`Dalvik`是`Google`公司自己设计用于`Android`平台的`Java`虚拟机。它可以支持已转换为`.dex`(即`Dalvik Executable`)格式的`Java`应用程序的运行, +`.dex`格式是专为`Dalvik`设计的一种压缩格式,适合内存和处理器速度有限的系统。`Dalvik`经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个`Dalvik`应用作为一个独立的`Linux`进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。 +很长时间以来,`Dalvik`虚拟机一直被用户指责为拖慢安卓系统运行速度不如`IOS`的根源。 +`2014`年`6`月`25`日,`Android L`正式亮相于召开的谷歌`I/O`大会,`Android L`改动幅度较大,谷歌将直接删除`Dalvik`,代替它的是传闻已久的`ART`。 + + + +早期,Android智能手机并不像现在那么强大。大多数手机的RAM很少,有些甚至只有200MB。 +难怪第一个被称为Dalvik的Android Runtime的实现正是为了优化此参数:RAM的使用。 + +因此,它没有在运行它之前将整个应用程序编译为机器代码,而是使用了称为Just In Time编译(简称JIT)的策略。 + +在这种策略下,编译器可以充当解释器。它在应用程序执行期间(在运行时)编译一小段代码。 + +而且由于Dalvik仅编译所需的代码并在运行时执行它,因此可以节省大量RAM。 + +使用Dalvik JIT编译器,每次运行该应用程序时,它会将Dalvik字节码的一部分动态转换为机器代码。随着执行的进行,更多的字节码将被编译和缓存。由于JIT仅编译部分代码,因此它具有较小的内存占用空间,并且在设备上使用的物理空间更少。 + +但是此策略有一个严重的缺点-因为所有这些都在运行时发生,因此显然会对运行时性能产生负面影响。 + +最终,引入了一些优化以使Dalvik更具性能。一些经常使用的已编译代码段已被缓存,不再重新编译。但这是非常有限的,因为在最初的日子里内存非常稀缺。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dalvik_jit.png) + +几年来运行良好,但与此同时,手机的性能越来越高,RAM也越来越多。而且由于应用程序也越来越大,因此JIT性能影响变得越来越成问题。 + +这就是为什么在Android L中引入了新的Android Runtime:ART。 + + + +### ART(Android L) + +`Android 4.4`提供了一种与`Dalvik`截然不同的运行环境`ART`支持,`ART`源于`google`收购的`Flexycore`的公司。 +`ART`模式与`Dalvik`模式最大的不同在于,启用`ART`模式后,系统在安装应用的时候会进行一次预编译,将字节码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。 + +`ART`使用`AOT(Ahead Of Time)`(静态编译)而`Dalvik`使用`JIT(Just In Time)`(动态编译) +`JIT`方式会在程序执行时将`Dex bytecode`(`java`字节码)转换为处理器可以理解的本地代码,这种方式会将编译时间计入程序的执行时间,程序执行会显得慢一些。`AOT`方式会在程序执行之前(一般是安装时)就编译好本地代码,因此程序执行时少了编译的过程会显得快一些,但占用更多存储空间,安装时也会更慢。但没针对ART优化的程序反而会运行得更慢,随着`Android L`的普及这个问题迟早会解决。`ART`拥有改进的`GC`(垃圾回收)机制:`GC`时更少的暂停时间、`GC`时并行处理、某些时候`Collector`所需时间更短、减少内存不足时触发GC的次数、减少后台内存占用。 +在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。总体的理念就是空间换时间。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/dvm_art.png) + +`AOT`的编译器分两种模式: + +- 在开发机上编译预装应用; +- 在设备上编译新安装的应用,在应用安装时将`dex`字节码翻译成本地机器码。 + + + +这种方法极大地提高了运行时性能,因为运行本机机器代码甚至比即时编译快20倍。 + +ART优点: + +- 系统性能的显著提升。 +- 应用启动更快、运行更快、体验更流畅、触感反馈更及时。 +- 更长的电池续航能力。 +- 支持更低的硬件。 + +ART缺点: + +- 更大的存储空间占用,可能会增加10%-20%。字节码预先编译成机器码并存储到本地,机器码需要的存储空间更大。 +- 更长的应用安装时间,因为下载APK后,整个应用程序都需要转换为机器代码,而且由于所有应用程序都需要重新优化,因此执行系统更新还需要更长的时间。 +- Android L中的ART使用的内存比Dalvik多得多。 + + + +对于应用程序中经常运行的部分来说,对其进行预编译显然是有回报的,但现实是,用户很少打开应用程序的大多数部分,而对整个应用程序进行预编译几乎也没有回报。 + +这就是为什么在Android N中,Just In Time编译与称为配置文件引导编译(profile-guided complication)一起被引入到Android Runtime的原因。 + +### Profile-guided compilation(Android N) + +配置文件引导编译是一种策略,可以在运行Android应用程序时不断提高其性能。默认情况下,应用程序使用即时编译策略进行编译,但是当ART检测到某些热点功能时,这意味着它们经常运行,ART可以预编译并缓存这些方法以获得最佳性能。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/art_profile_guide.png) + +该策略可为应用程序的关键部分提供最佳性能,同时减少RAM使用量。由于事实证明,对于大多数应用程序来说,通常仅使用10%到20%的代码。 + +更改ART之后,不再影响应用程序安装和系统更新的速度。应用的关键部分的预编译仅在设备空闲和充电时进行,以最大程度地减少对设备电池的影响。 + + + +这种方法的唯一缺点是,为了获取配置文件数据并预编译常用的方法和类,用户必须实际使用应用程序。这意味着该应用程序的一些首次使用可能会有点慢,因为在这种情况下,将仅使用即时编译。 + +这就是为什么要改善在Android P中的初始用户体验的原因,Google在云中引入了个人资料。 + +### Profiles in the cloud(Android P) + +云中的配置文件背后的主要思想是,大多数人以非常相似的方式使用该应用程序。因此,为了在安装后立即提高性能,我们可以从已经使用过此应用程序的人那里收集配置文件数据。此汇总的配置文件数据用于为应用程序创建一个称为通用核心配置文件的文件。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/profiles_in_cloud.png) + +因此,当新用户安装该应用程序时,该文件将与该应用程序一起下载。 ART使用它来预编译大多数用户经常运行的类和方法。这样,新用户在下载应用程序后即可获得更好的性能。 + +这并不意味着不再使用旧策略。用户运行应用程序后,ART将收集用户特定的配置文件数据并重新编译设备闲置时该特定用户经常使用的代码。 + +而最好的部分是,我们开发应用程序的开发人员无需执行任何操作即可启用此功能。这一切都发生在Android Runtime中。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_runtime_change.png) + + + +- 最初Android使用Dalvik,它使用即时编译(JIT)来优化RAM(那时内存是非常紧缺的)的使用。 +- 为了提高Android L中的性能,引入了使用Ahead of time编译的ART。这样可以实现更好的运行时性能,但会导致更长的安装时间和更多的RAM使用率。 +- 因此,在Android N中,JIT被引入到ART中,并且配置文件引导的编译允许为经常运行的部分代码提供更好的性能。 +- 为了让用户在Android P中安装应用后立即获得最佳性能,Google在云端引入了配置文件,它通过添加随APK下载的通用核心配置文件来补充以前的优化,并允许ART预编译部分代码最常由以前的应用程序用户运行。 + + + + + +[Android Runtime-How Dalvik and ART works?](https://www.youtube.com/watch?v=0J1bm585UCc&t=27s) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" "b/AdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" similarity index 95% rename from "Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" rename to "AdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" index 95bc3f31..b6b73b52 100644 --- "a/Android\345\212\240\345\274\272/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" +++ "b/AdavancedPart/Android\345\215\270\350\275\275\345\217\215\351\246\210.md" @@ -129,8 +129,11 @@ void Java_com_charon_uninstallfeedback_MainActivity_initUninstallFeedback( - 编译so文件。`Windows`下要用`cygwin`来操作。 上面的介绍是在`Eclipse`中进行的,用`ndk-build`命令来编译`so`。具体请看之前写的`JNI基础`这篇文章。 -有关如何在`Android Stuido`中进行`ndk`开发请看另一篇文章。 +有关如何在[Android Stuido中进行ndk开发请看][1]。 + +[1]: https://github.com/CharonChui/AndroidNote/blob/master/AndroidStudioCourse/AndroidStudio%E4%B8%AD%E8%BF%9B%E8%A1%8Cndk%E5%BC%80%E5%8F%91.md "Android Stuido中进行ndk开发" + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" similarity index 65% rename from "Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" rename to "AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" index 4b2e9aa4..5008840f 100644 --- "a/Android\345\212\240\345\274\272/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" +++ "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -1,11 +1,11 @@ Android启动模式详解 === -- standard - 默认模式。在该模式下,`Activity`可以拥有多个实例,并且这些实例既可以位于同一个task,也可以位于不同的task。每次都会新创建。 -- singleTop +- `standard` + 默认模式。在该模式下,`Activity`可以拥有多个实例,并且这些实例既可以位于同一个`task`,也可以位于不同的`task`。每次都会新创建。 +- `singleTop` 该模式下,在同一个`task`中,如果存在该`Activity`的实例,并且该`Activity`实例位于栈顶则不会创建该`Activity`的示例,而仅仅只是调用`Activity`的`onNewIntent()`。否则的话,则新建该`Activity`的实例,并将其置于栈顶。 -- singleTask +- `singleTask` 顾名思义,只容许有一个包含该`Activity`实例的`task`存在! 在`android`浏览器`browser`中,`BrowserActivity`的`launcherMode="singleTask"`,因为`browser`不断地启动自己,所以要求这个栈中保持只能有一个自己的实例,`browser`上网的时候, 遇到播放视频的链接,就会通过隐式`intent`方式跳转找`Gallery3D`中的`MovieView`这个类来播放视频,这时候如果你点击`home`键,再点击`browser`,你会发现`MovieView`这个类已经销毁不存在了, @@ -16,30 +16,50 @@ Android启动模式详解 以`singleTask`方式启动的`Activity`,全局只有唯一个实例存在,因此,当我们第一次启动这个`Activity`时,系统便会创建一个新的任务栈,并且初始化一个`Activity`实例,放在新任务栈的底部,如果下次再启动这个`Activity`时, 系统发现已经存在这样的`Activity`实例,就会调用这个`Activity`实例的`onNewIntent`方法,从而把它激活起来。从这句话就可以推断出,以`singleTask`方式启动的`Activity`总是属于一个任务栈的根`Activity`。 下面我们看一下示例图:  - ![image](https://github.com/CharonChui/Pictures/blob/master/singletask.gif?raw=true) + ![image](https://github.com/CharonChui/Pictures/blob/master/singletask.gif?raw=true) 坑爹啊!有木有!前面刚说`singleTask`会在新的任务中运行,并且位于任务堆栈的底部,这里在`Task B`中,一个赤裸裸的带着`singleTask`标签的箭头无情地指向`Task B`堆栈顶端的`Activity Y`,什么鬼? 这其实是和`taskAffinity`有关,在将要启动时,系统会根据要启动的`Activity`的`taskAffinity`属性值在系统中查找这样的一个`Task`:`Task`的`affinity`属性值与即将要启动的`Activity`的`taskAffinity`属性值一致。如果存在, 就返回这个`Task`堆栈顶端的`Activity`回去,不重新创建任务栈了,再去启动另外一个`singletask`的`activity`时就会在跟它有相同`taskAffinity`的任务中启动,并且位于这个任务的堆栈顶端,于是,前面那个图中, 就会出现一个带着`singleTask`标签的箭头指向一个任务堆栈顶端的`Activity Y`了。在上面的`AndroidManifest.xml`文件中,没有配置`MainActivity`和`SubActivity`的`taskAffinity`属性, 于是它们的`taskAffinity`属性值就默认为父标签`application`的`taskAffinity`属性值,这里,标签`application`的`taskAffinity`也没有配置,于是它们就默认为包名。 -总的来说:`singleTask`的结论与`android:taskAffinity`相关:   +总的来说:`singleTask`的结论与`android:taskAffinity`相关:    - 设置了`singleTask`启动模式的`Activity`,它在启动的时候,会先在系统中查找属性值`affinity`等于它的属性值`taskAffinity`的任务栈的存在;如果存在这样的任务栈,它就会在这个任务栈中启动,否则就会在新任务栈中启动。 因此,如果我们想要设置了`singleTask`启动模式的`Activity`在新的任务栈中启动,就要为它设置一个独立的`taskAffinity`属性值。以`A`启动`B`来说当`A`和`B`的`taskAffinity`相同时:第一次创建`B`的实例时,并不会启动新的`task`, 而是直接将`B`添加到`A`所在的`task`;否则,将`B`所在`task`中位于`B`之上的全部`Activity`都删除,然后跳转到`B`中。 - 如果设置了`singleTask`启动模式的`Activity`不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的`Activity`实例,如果存在,就会把位于这个`Activity`实例上面的`Activity`全部结束掉, 即最终这个Activity实例会位于任务的堆栈顶端中。以`A`启动`B`来说,当`A`和`B`的`taskAffinity`不同时:第一次创建`B`的实例时,会启动新的`task`,然后将`B`添加到新建的`task`中;否则,将`B`所在`task`中位于`B`之上的全部`Activity`都删除,然后跳转到`B`中。 -- singleInstance +- `singleInstance` + 顾名思义,是单一实例的意思,即任意时刻只允许存在唯一的`Activity`实例,而且该`Activity`所在的`task`不能容纳除该`Activity`之外的其他`Activity`实例! -它与`singleTask`有相同之处,也有不同之处。 -相同之处:任意时刻,最多只允许存在一个实例。 -不同之处: - - `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 - - `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 - - 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时, - 不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。 +它与`singleTask`有相同之处,也有不同之处。 +相同之处: 任意时刻,最多只允许存在一个实例。 +不同之处: + +- `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 +- `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 +- 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时,不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。 + + +假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢? + +使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。 +而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。 + +假设现在有FirstActivity、SecondActivity、ThirdActivity三个Activity, SecondActivity的启动模式是SingleInstance。 +现在FirstActivity 启动SecondActivity,SecondActivity再启动ThirdActivity。 + +然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?其实原理很简单,由于FirstActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键时,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶Activity显示在界面上,因此也就出现了从ThirdActivity直接返回到FirstActivity的情况。然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶Activity,即SecondActivity。最后再次按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_launch_mode_singleinstance.png?raw=true) + + + + + --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" "b/AdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" new file mode 100644 index 00000000..102de73a --- /dev/null +++ "b/AdavancedPart/Android\345\274\200\345\217\221\344\270\215\347\224\263\350\257\267\346\235\203\351\231\220\346\235\245\344\275\277\347\224\250\345\257\271\345\272\224\345\212\237\350\203\275.md" @@ -0,0 +1,56 @@ +Android开发不申请权限来使用对应功能 +=== + +从用户角度来说很难获取到正确的`android`权限。通常你只需要做一些很基础的事(例如编辑一个联系人)但实际你申请的权限却远远比这更强大(例如可以获取到所有的联系人明细等)。 + +这样能很容易的理解到用户会怀疑到你的应用。如果你的应用不是开源的,那他们就没有方法来验证你会不会下载所有的联系人数据并上传到服务器。及时你去解释为什么需要这个权限,但是人们不会相信你。原来我会选择不去使用这些敏感的权限来防止用户产生不信任。(当然很多应用他们申请权限只是为了后台获取你的联系人数据上传- -!以及之前被爆的某宝使用摄像头拍照的问题) + +这就是说,有一件事在困扰着我,**如何能在做一些操作时不去申请权限。** + +打个比方说:`android.permission.CALL_PHONE`这个权限。你需要它来在应用中拨打电话,是吗?这就是你怎么去实现拨号的吗? +```java +Intent intent = new Intent(Intent.ACTION_CALL); +intent.setData(Uri.parse("tel:1234567890")) +startActivity(intent); +``` +错!,你通过这段代码需要该权限的原因是因为你可以在任何时间在不需要用户操作的情况下打电话。也就是说如果我的应用申请了这个权限,我可以在你不知情的情况下每天凌晨三点去拨打骚扰电话。 + +正确的方式是使用`ACTION_VIEW`或者`ACTION_DIAL`: +```java +Intent intent = new Intent(Intent.ACTION_DIAL); +intent.setData(Uri.parse("tel:1234567890")) +startActivity(intent); +``` + +这个问题的完美解决方案就是不需要申请权限了。原因就是你不是直接拨号,而是用指定的号码调起拨号器,仍然需要用户点击”拨号”来开始打电话。老实的说,这样让人感觉更好。 + +简单的说就是如果我想要的操作不是让用户在应用内点击某个按钮就直接开始拨打电话,而是让用户点击在应用内点击某个按钮是我们去调起拨号程序,并且显示指定号码,让用户在拨号器中点击拨号后再开始拨打电话。这样的话我们就完全不用申请拨号权限了。 + +另一个例子: 我想获取某一个联系人的号码,你可能会想这需要申请获取所有联系人的权限。这是错的!。 +```java +Intent intent = new Intent(Intent.ACTION_PICK); +intent.setType(StructuredPostal.CONTENT_TYPE); +startActivityForResult(intent, 1); +``` +我们可以使用上面的代码,来启动联系人管理器,让用户来选择某一个联系人。这样不仅是不需要申请任何权限,也不需要提供任何联系人相关的`UI`。这样也能完全保证你选择联系人时的体验。 + + +`Android`系统最酷的部分之一就是`Intent`系统,这意味着我不需要自己来实现所有的东西。应用可以注册处理它所擅长的指定数据,像电话号码、短信或者联系人。如果这些都要自己在一个应用中去实现,那这将会是很大的工作量,也会让应用变得臃肿。 + +`Android`系统的另一个优势就是你可以使用其他应用申请的权限,而不用自己申请。这样才保证了上面的情况。拨号器需要申请拨打电话的权限,我只需要一个能调起拨号器的`Intent`就好了。用户信任拨号器拨打电话,而不是我们的应用。他们无论如何都宁愿使用系统的拨号器。 + +写这篇文章的意义是**在你想要申请一个权限的时候,你需要至少看看[Intent的官方文档](https://developer.android.com/reference/android/content/Intent.html)看能否请求另外一个应用来帮我们做这些操作。**如果想要深入的研究,可以学习下[关于权限的详细介绍](https://developer.android.com/guide/topics/security/permissions.html),这里面包含了很多精细的权限。 + +使用更少的权限可以不但可以让用户更加信任你,而且可以让用户有一个更好的体验,因为他们仍然在使用他们所期望的应用。 + +1. 遗憾的是,不是一个真实的号码。 +2. 不幸的是,`Intent`系统的属性也建立了[可能会被滥用的漏洞](http://css.csail.mit.edu/6.858/2012/projects/ocderby-dennisw-kcasteel.pdf),但你也不会写一个滥用的应用,是吗? + + +- (译)[感谢Dan Lew](http://blog.danlew.net/2014/11/26/i-dont-need-your-permission/) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" new file mode 100644 index 00000000..4653b0a9 --- /dev/null +++ "b/AdavancedPart/Android\345\274\200\345\217\221\344\270\255\347\232\204MVP\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -0,0 +1,600 @@ +Android开发中的MVP模式详解 +=== + +[MVC、MVP、MVVM介绍][1] + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_mvp.jpg?raw=true) + +在`Android`开发中,如果不注重架构的话,`Activity`类就会变得愈发庞大。这是因为在`Android`开发中`View`和其他的线程可以共存于`Activity`内。那最大的问题是什么呢? 其实就是`Activity`中同时存在业务逻辑和`UI`逻辑。这导致增加了单元测试和维护的成本。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_is_god.png?raw=true) + +这就是为什么要清晰架构的原因之一。不仅是因为`Activity`类变得臃肿,也是其他的一些问题,例如`Activity`和`Fragment`相结合时的生命周期、数据绑定等等。 + +### MVP简介 + +`MVP(Model,View,Presenter)` + +- `View`:负责处理用户时间和视图展现。在`Android`中就可能是`Activity`或者`Fragment`。 +- `Model`: 负责数据访问。数据可以是从接口或者本地数据库中获取。 +- `Presenter`: 负责连接`View`和`Model`。 + +用一句话来说:`MVP`其实就是面向接口编程,`V`实现接口,`P`使用接口。 + +清晰的架构: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mvp_a.png?raw=true) + + +举个栗子: +在`Android Studio`中新建一个`Activity`,系统提供了`LoginActivity`,直接用它是极好的。 + + + +不得不说,`Material Design`的效果真是美美哒! + +好,那我们就用用户登录页来按照`MVP`的模式实现一下: + +- M: 很显然Model应该是`User`类。 +- V: `View`就是`LoginActivity`。 +- P: P那我们一会就创建一个`LoginPresenter`类。 + +齐了,那接下来就详细分析下他们这三部分: + +- `User`: 应该有`email`, `password`, `boolean login(email, password)`。 +- `LoginActivity`:点击登录应该要出`loading`页。登录成功后要进入下一个页面。如果登录失败应该弹`toast`提示。那就需要`void showLoading()`,`void hideLoading()`,`void showErrorTip()`,`void doLoginSuccess()`这四个方法。 +- `LoginPresenter`:这是`Model`和`View`的桥梁。他需要做的处理业务逻辑,直接与`Model`打交道,然后将`UI`的逻辑交给`LoginActivity`处理。 +那怎么做呢? 按照我上面总结的那一句话,、`MVP`其实就是面向接口编程,`V`实现接口,`P`使用接口。很显然我们需要提供一个接口。那就新建一个`ILoginView`的接口。这里面有哪些方法呢? 当然是上面我们在分析`LoginActiity`时提出的那四个方法。这样`LoginActivity`直接实现`ILoginView`接口就好。 + + +开始做: + +- 先把`Model`做好吧,创建`User`类。 + + ```java + public class User { + private String email; + private String password; + public User(String email, String password) { + this.email = email; + this.password = password; + } + + public boolean login() { + // do login request.. + return true; + } + } + ``` + +- 创建`ILoginView`接口,定义登录所需要的`ui`逻辑。 + + ```java + public interface ILoginView { + void showLoading(); + void hideLoading(); + void showErrorTip(); + void doLoginSuccess(); + } + ``` + +- 创建`LoginPresenter`类,使用`ILoginView`接口,那该类主要有什么功能呢? 它主要是处理业务逻辑的, + 对于登录的话,当然是用户在`UI`页面输入邮箱和密码,然后`Presenter`去开线程、请求接口。然后得到登录结果再去让`UI`显示对应的视图。那自然就是有一个`void login(String email, String passowrd)`的方法了 + + ```java + public class LoginPresenter { + private ILoginView mLoginView; + + public LoginPresenter(ILoginView loginView) { + mLoginView = loginView; + } + + public void login(String email, String password) { + if (TextUtils.isEmpty(email) || TextUtils.isEmpty(password)) { + // + mLoginView.showErrorTip(); + return; + } + mLoginView.showLoading(); + User user = new User(email, password); + + // do network request.... + // .... + onSuccess() { + boolean login = user.login(); + if (login) { + mLoginView.doLoginSuccess(); + } else { + mLoginView.showErrorTip(); + } + mLoginView.hideLoading(); + } + + onFailde() { + mLoginView.showErrorTip(); + mLoginView.hideLoading(); + } + } + } + ``` +- 创建`LoginActivity`,实现`ILoginView`的接口,然后内部调用`LoginPresenter`来处理业务逻辑。 + + ```java + public class LoginActivity extends AppCompatActivity implements ILoginView { + private LoginPresenter mLoginPresenter; + + private AutoCompleteTextView mEmailView; + private EditText mPasswordView; + private View mProgressView; + private View mLoginButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + mEmailView = (AutoCompleteTextView) findViewById(R.id.email); + mPasswordView = (EditText) findViewById(R.id.password); + mLoginButton = findViewById(R.id.email_sign_in_button); + mProgressView = findViewById(R.id.login_progress); + + mLoginPresenter = new LoginPresenter(this); + + mLoginButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mLoginPresenter.login(mEmailView.getText().toString().trim(), mPasswordView.getText().toString().trim()); + } + }); + } + + @Override + public void showLoading() { + mProgressView.setVisibility(View.VISIBLE); + } + + @Override + public void hideLoading() { + mProgressView.setVisibility(View.GONE); + } + + @Override + public void showErrorTip() { + Toast.makeText(this, "login faled", Toast.LENGTH_SHORT).show(); + } + + @Override + public void doLoginSuccess() { + Toast.makeText(this, "login success", Toast.LENGTH_SHORT).show(); + } + } + ``` + +--- + + +上面只是抛砖引玉。`MVP`的优点十分明显,就是代码解耦、可以让逻辑清晰,但是同样它也会有缺点,它的缺点就是项目的复杂程度会增加,项目中会多出很多类。 +之前很多人都在讨论该如何去正确的设计使用`MVP`来避免它的缺点,众说纷纭,很多人讨论的你死我活。直到`Google`发布了`MVP架构蓝图`,大家才意识到这才是规范。 + +项目地址:[android-architecture](https://github.com/googlesamples/android-architecture) +`Google`将该项目命名为`Android`的架构蓝图,我想从名字上已可以看穿一切。 + +在它的官方介绍中是这样说的: + +> The Android framework offers a lot of flexibility when it comes to defining how to organize and architect an Android app. This freedom, whilst very valuable, can also result in apps with large classes, inconsistent naming and architectures (or lack of) that can make testing, maintaining and extending difficult. + +> Android Architecture Blueprints is meant to demonstrate possible ways to help with these common problems. In this project we offer the same application implemented using different architectural concepts and tools. + +> You can use these samples as a reference or as a starting point for creating your own apps. The focus here is on code structure, architecture, testing and maintainability. However, bear in mind that there are many ways to build apps with these architectures and tools, depending on your priorities, so these shouldn't be considered canonical examples. The UI is deliberately kept simple. + + + +已完成的示例: + +- todo-mvp/ - Basic Model-View-Presenter architecture. +- todo-mvp-loaders/ - Based on todo-mvp, fetches data using Loaders. +- todo-mvp-databinding/ - Based on todo-mvp, uses the Data Binding Library. +- todo-mvp-clean/ - Based on todo-mvp, uses concepts from Clean Architecture. +- todo-mvp-dagger/ - Based on todo-mvp, uses Dagger2 for Dependency Injection +- todo-mvp-contentproviders/ - Based on todo-mvp-loaders, fetches data using Loaders and uses Content Providers +- todo-mvp-rxjava/ - Based on todo-mvp, uses RxJava for concurrency and data layer abstraction. + + +我们接下来就用`todo-mvp`来进行分析,这个应用非常简单,主要有以下几个功能: + +- 列表页:展示所有的`todo`项 +- 添加页:添加`todo`项 +- 详情页:查看`todo`项的详情 +- 统计页:查看当前所有已完成`todo`及未完成项的统计数据 + +代码并不多: + + + + +功能也比较简单: + + + + +我们先从两个`Base`类开始看,分别是`BaseView`以及`BasePresenter`类。 + +`BaseView`类: + +```java +public interface BaseView { + + void setPresenter(T presenter); + +} +``` +`BaseView`中的`setPresenter()`是将`Presenter`的实例传入到`View`中。 +`BasePresenter`类: +```java +public interface BasePresenter { + + void start(); + +} +``` +`BasePresenter`中只有一个`start()`方法,从名字上我们就能看出他的作用。 + +接下来继续看一下项目的入口`Activity`,`TaskDetailActivity`的实现: +```java +public class TaskDetailActivity extends AppCompatActivity { + + public static final String EXTRA_TASK_ID = "TASK_ID"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.taskdetail_act); + + // Set up the toolbar. + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar ab = getSupportActionBar(); + ab.setDisplayHomeAsUpEnabled(true); + ab.setDisplayShowHomeEnabled(true); + + // Get the requested task id + String taskId = getIntent().getStringExtra(EXTRA_TASK_ID); + + TaskDetailFragment taskDetailFragment = (TaskDetailFragment) getSupportFragmentManager() + .findFragmentById(R.id.contentFrame); + + if (taskDetailFragment == null) { + // 创建对应的Fragment + taskDetailFragment = TaskDetailFragment.newInstance(taskId); + + ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), + taskDetailFragment, R.id.contentFrame); + } + + // Create the presenter,因为Presenter是M和V的连接桥,所以在第二个参数中会传入TasksRepository + new TaskDetailPresenter( + taskId, + Injection.provideTasksRepository(getApplicationContext()), + taskDetailFragment); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } +} +``` + +可以看到这里`Activity`相当于一个管理类,里面控制着`MVP`中的`V`和`P`,上面的代码主要做了两部分: + +- 创建对应的`Fragment` +- 创建对应的`Presenter` + +这里分别看一下`TaskDetailFragment`和`TaskDetailPresenter`的源码: +```java +public class TaskDetailFragment extends Fragment implements TaskDetailContract.View { + private TaskDetailContract.Presenter mPresenter; + + public static TaskDetailFragment newInstance(@Nullable String taskId) { + Bundle arguments = new Bundle(); + arguments.putString(ARGUMENT_TASK_ID, taskId); + TaskDetailFragment fragment = new TaskDetailFragment(); + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onResume() { + super.onResume(); + // 调用Presenter.start()方法 + mPresenter.start(); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + .... + } + + @Override + public void setPresenter(@NonNull TaskDetailContract.Presenter presenter) { + // 将Prsenter传递给V中 + mPresenter = checkNotNull(presenter); + } + + .... +} +``` +及 +```java +public class TaskDetailPresenter implements TaskDetailContract.Presenter { + private final TasksRepository mTasksRepository; + + private final TaskDetailContract.View mTaskDetailView; + + @Nullable + private String mTaskId; + + // 把M和V传递进来 + public TaskDetailPresenter(@Nullable String taskId, + @NonNull TasksRepository tasksRepository, + @NonNull TaskDetailContract.View taskDetailView) { + mTaskId = taskId; + mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!"); + mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!"); + // 将该Presenter设置给V + mTaskDetailView.setPresenter(this); + } + + @Override + public void start() { + // 调用获取数据的方法 + openTask(); + } + .... +} +``` + +可以看到上面分别实现了`TaskDetailContract`类中的`Presenter`和`View`接口,而不是`BasePresenter`和`BaseView`中的接口,那这个`TaskDetailContract`类是什么呢? +```java +/** + * This specifies the contract between the view and the presenter. + */ +public interface TaskDetailContract { + + interface View extends BaseView { + + void setLoadingIndicator(boolean active); + + void showMissingTask(); + + void hideTitle(); + + void showTitle(String title); + + void hideDescription(); + + void showDescription(String description); + + void showCompletionStatus(boolean complete); + + void showEditTask(String taskId); + + void showTaskDeleted(); + + void showTaskMarkedComplete(); + + void showTaskMarkedActive(); + + boolean isActive(); + } + + interface Presenter extends BasePresenter { + + void editTask(); + + void deleteTask(); + + void completeTask(); + + void activateTask(); + } +} +``` +按照上面描述的介绍可以看出通过这个连接类将`VP`联系到了一起,它统一管理`view`和`presenter`中的所有接口,这样比起分开写会更加清晰。 + +分析完`VP`之后我们继续看一下`TaskDetailPresenter`类,因为`Presenter`是`MV`的桥梁。 +```java +public TaskDetailPresenter(@Nullable String taskId, + @NonNull TasksRepository tasksRepository, + @NonNull TaskDetailContract.View taskDetailView) { + mTaskId = taskId; + mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!"); + mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!"); + + mTaskDetailView.setPresenter(this); + } + + @Override + public void start() { + openTask(); + } + + private void openTask() { + if (Strings.isNullOrEmpty(mTaskId)) { + mTaskDetailView.showMissingTask(); + return; + } + // 控制V显示UI + mTaskDetailView.setLoadingIndicator(true); + // 通过M去获取数据 + mTasksRepository.getTask(mTaskId, new TasksDataSource.GetTaskCallback() { + @Override + public void onTaskLoaded(Task task) { + // The view may not be able to handle UI updates anymore + if (!mTaskDetailView.isActive()) { + return; + } + mTaskDetailView.setLoadingIndicator(false); + if (null == task) { + mTaskDetailView.showMissingTask(); + } else { + showTask(task); + } + } + + @Override + public void onDataNotAvailable() { + // The view may not be able to handle UI updates anymore + if (!mTaskDetailView.isActive()) { + return; + } + mTaskDetailView.showMissingTask(); + } + }); + } + +``` +在它的构造函数中传入了一个`TasksRepository`,这个就是`Model`层。而在该`Presenter`中的`start()`方法回去调用获取数据的方法。 + +我们继续看一下`M`层`TasksRepository`的实现: +```java +/** + * Concrete implementation to load tasks from the data sources into a cache. + *

+ * For simplicity, this implements a dumb synchronisation between locally persisted data and data + * obtained from the server, by using the remote data source only if the local database doesn't + * exist or is empty. + */ +public class TasksRepository implements TasksDataSource { + .... +} +``` + +先看一下`TasksDataSource`接口: + +```java +/** + * Main entry point for accessing tasks data. + *

+ * For simplicity, only getTasks() and getTask() have callbacks. Consider adding callbacks to other + * methods to inform the user of network/database errors or successful operations. + * For example, when a new task is created, it's synchronously stored in cache but usually every + * operation on database or network should be executed in a different thread. + */ +public interface TasksDataSource { + + interface LoadTasksCallback { + + void onTasksLoaded(List tasks); + + void onDataNotAvailable(); + } + + interface GetTaskCallback { + + void onTaskLoaded(Task task); + + void onDataNotAvailable(); + } + + void getTasks(@NonNull LoadTasksCallback callback); + + void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback); + + void saveTask(@NonNull Task task); + + void completeTask(@NonNull Task task); + + void completeTask(@NonNull String taskId); + + void activateTask(@NonNull Task task); + + void activateTask(@NonNull String taskId); + + void clearCompletedTasks(); + + void refreshTasks(); + + void deleteAllTasks(); + + void deleteTask(@NonNull String taskId); +} + +``` + +可以看出`M`层被赋予了数据获取的功能,与之前我们写的`M`层只定义实体对象截然不同,数据的增删改查都是`M`层实现的。`Presenter`只需要调用`M`层的方法即可。这样`model、presenter、view`都只处理各自的任务,此种实现确实是单一职责最好的诠释。 + +这里我们就以上面用到的`getTasks()`方法为例来介绍一下: +```java +public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) { + checkNotNull(taskId); + checkNotNull(callback); + // 从缓存中获取数据 + Task cachedTask = getTaskWithId(taskId); + + // Respond immediately with cache if available + if (cachedTask != null) { + callback.onTaskLoaded(cachedTask); + return; + } + + // Load from server/persisted if needed. + + // Is the task in the local data source? If not, query the network. + mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() { + @Override + public void onTaskLoaded(Task task) { + // Do in memory cache update to keep the app UI up to date + if (mCachedTasks == null) { + mCachedTasks = new LinkedHashMap<>(); + } + mCachedTasks.put(task.getId(), task); + callback.onTaskLoaded(task); + } + + @Override + public void onDataNotAvailable() { + // 服务器的数据源 + mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() { + @Override + public void onTaskLoaded(Task task) { + // Do in memory cache update to keep the app UI up to date + if (mCachedTasks == null) { + mCachedTasks = new LinkedHashMap<>(); + } + mCachedTasks.put(task.getId(), task); + callback.onTaskLoaded(task); + } + + @Override + public void onDataNotAvailable() { + callback.onDataNotAvailable(); + } + }); + } + }); + } +``` + +上面的注释说的非常明白了,这里就不去仔细看了。 + + +下面用一张图来总结一下: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/google_todo_mvp.png?raw=true) + + +由于架构的引入,虽然代码量有了一定的上升,但是功能分离的非常清晰明确,而且每个部分都可以进行单独的测试,对于后期的扩展维护会更加简单容易。 + + + +[1]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/MVC%E4%B8%8EMVP%E5%8F%8AMVVM.md "MVC、MVP、MVVM介绍" + +--- + + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git a/AdavancedPart/ApplicationId vs PackageName.md b/AdavancedPart/ApplicationId vs PackageName.md new file mode 100644 index 00000000..35dc2f0a --- /dev/null +++ b/AdavancedPart/ApplicationId vs PackageName.md @@ -0,0 +1,89 @@ +ApplicationId vs PackageName +=== + +曾几何时,自从转入`Studio`阵营后就发现多了个`applicationId "com.xx.xxx"`,虽然知道肯定会有区别,但是我却没有仔细去看,只想着把它和`packageName`设置成相同即可(原谅我的懒惰- -!)。 + +直到今天在官网看`Gradle`使用时,终于忍不住要搞明白它俩的区别。 + +在`Android`官方文档中有一句是这样描述`applicationId`的:`applicationId : the effective packageName`,真是言简意赅,那既然`applicationId`是有效的包明了,`packageName`算啥? + +所有`Android`应用都有一个包名。包名在设备上能唯一的标识一个应用,它在`Google Play`应用商店中也是唯一的。这就意味着一旦你使用一个包名发布应用后,你就永 远不能改变它的包名;如果你改了包名就会导致你的应用被认为是一个新的应用,并且已经使用你之前应用的用户将不会看到作为更新的新应用包。 + +之前的`Android Gradle`构建系统中,应用的包名是由你的`manifest`文件中的根元素中的`package`属性定义的: + +`AndroidManifest.xml: ` + +``` + + +``` +然而,这里定义的包也有第二个目的:就是被用来命名你的`R`资源类(以及解析任何与`Activities`相关的类名)的包。在上面的示例中,生成的`R`类就是`com.example.my.app.R`,所以如果你在其他的包中想引用资源,就需要导入`com.example.my.app.R`。 + +伴随着新的`Android Gradle`构建系统,你可以很简单的为你的应用构建多个不同的版本;例如,你可以同时为你的应用构建一个免费版本和一个专业版(使用`flavors`),并且他们应该在`Google Play`商店中有不同的包,这样才能让他们可以被单独安装和购买,同时安装两个,等等。同样的你也可能同时为你的应用构建`debug`版、`alpha`版和`beta`版(使用`build types`),这些也可以同样使用不同的包名。 + +在这同时,你在代码中导入的`R`类必须一直保持一直;在为应用构建不同的版本时`.java`源文件都不应该发生变化。 + +因此,我们解耦了`package name`的两种用法: + +- 在生成的`.apk`中的`manifest`文件中使用的最终的包名以及在你的设备和`Google Play`商店中用来标示你的包名叫做`application id`的值。 +- 在源代码中指向`R`类的包名以及在解析任何与`activity/service`注册相关的包名继续叫做`package name`。 + +可以在`gradle`文件中像如下指定`application id`: + +`app/build.gradle:` + +``` +apply plugin: 'com.android.application' + +android { + compileSdkVersion 19 + buildToolsVersion "19.1" + + defaultConfig { + applicationId "com.example.my.app" + minSdkVersion 15 + targetSdkVersion 19 + versionCode 1 + versionName "1.0" + } + ... +``` + +像之前一样,你需要在`Manifest`文件中指定你在代码中使用的`package name`,像上面`AndroidManifest.xml`的例子。 +下面进入关键部分了:当你按照上面的方式做完后,这两个包就是相互独立的了。你现在可以很简单的重构你的代码-通过修改`Manifest`中的包名来修改在你的`activitise`和`services`中使用的包和在重构你在代码中的引用声明。这不会影响你应用的最终`id`,也就是在`Gradle`文件中的`applicationId`。 + +你可以通过以下`Gradle DSL`方法为应用的`flavors`和`build types`指定不同的`applicationId`: + +`app/buid.gradle: ` + +``` +productFlavors { + pro { + applicationId = "com.example.my.pkg.pro" + } + free { + applicationId = "com.example.my.pkg.free" + } +} + +buildTypes { + debug { + applicationIdSuffix ".debug" + } +} +.... +``` +(在`Android Studio`中你也可以通过图形化的`Project Structure`的对话框来更改上面所有的配置) + +注意:为了兼容性,如果你在`build.gradle`文件中没有定义`applicationId` ,那`applicationId`就是与`AndroidManifest.xml`中配置的包名相同的默认值。在这种情况下,这两者显然脱不了干系,如果你试图重构代码中的包就将会导致同时会改变你应用程序的`id`!在`Android Studio`中新创建的项目都是同时指定他们俩。 + +注意2:`package name`必须在默认的`AndroidManifest.xml`文件中指定。如果有多个`manifest`文件(例如对每个`flavor`制定一个`manifest`或者每个`build type`制定一个`manifest`)时,`package name`是可选的,但是如果你指定的话,它必须与主`manifest`中指定的`pakcage`相同。 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Android\345\212\240\345\274\272/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" "b/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" similarity index 95% rename from "Android\345\212\240\345\274\272/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" rename to "AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" index 29689919..0a6a7f1a 100644 --- "a/Android\345\212\240\345\274\272/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" +++ "b/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" @@ -1,49 +1,49 @@ -BroadcastReceiver安全问题 -=== - -`BroadcastReceiver`设计的初衷是从全局考虑可以方便应用程序和系统、应用程序之间、应用程序内的通信,所以对单个应用程序而言`BroadcastReceiver`是存在安全性问题的(恶意程序脚本不断的去发送你所接收的广播) -- 保证发送的广播要发送给指定的对象 - 当应用程序发送某个广播时系统会将发送的`Intent`与系统中所有注册的`BroadcastReceiver`的`IntentFilter`进行匹配,若匹配成功则执行相应的`onReceive`函数。可以通过类似`sendBroadcast(Intent, String)`的接口在发送广播时指定接收者必须具备的`permission`或通过`Intent.setPackage`设置广播仅对某个程序有效。 - -- 保证我接收到的广播室指定对象发送过来的 - 当应用程序注册了某个广播时,即便设置了`IntentFilter`还是会接收到来自其他应用程序的广播进行匹配判断。对于动态注册的广播可以通过类似`registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)`的接口指定发送者必须具备的`permission`,对于静态注册的广播可以通过`android:exported="false"`属性表示接收者对外部应用程序不可用,即不接受来自外部的广播。 - -`android.support.v4.content.LocalBroadcastManager`工具类,可以实现在自己的进程内进行局部广播发送与注册,使用它比直接通过sendBroadcast(Intent)发送系统全局广播有以下几个好处: -- 因广播数据在本应用范围内传播,你不用担心隐私数据泄露的问题。 -- 不用担心别的应用伪造广播,造成安全隐患。 -- 相比在系统内发送全局广播,它更高效。 - -```java - LocalBroadcastManager mLocalBroadcastManager; - BroadcastReceiver mReceiver; - -@Override -protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - IntentFilter filter = new IntentFilter(); - filter.addAction("test"); - - mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals("test")) { - //Do Something - } - } - }; - mLocalBroadcastManager = LocalBroadcastManager.getInstance(this); - mLocalBroadcastManager.registerReceiver(mReceiver, filter); -} - - -@Override -protected void onDestroy() { - mLocalBroadcastManager.unregisterReceiver(mReceiver); - super.onDestroy(); -} -``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +BroadcastReceiver安全问题 +=== + +`BroadcastReceiver`设计的初衷是从全局考虑可以方便应用程序和系统、应用程序之间、应用程序内的通信,所以对单个应用程序而言`BroadcastReceiver`是存在安全性问题的(恶意程序脚本不断的去发送你所接收的广播) +- 保证发送的广播要发送给指定的对象 + 当应用程序发送某个广播时系统会将发送的`Intent`与系统中所有注册的`BroadcastReceiver`的`IntentFilter`进行匹配,若匹配成功则执行相应的`onReceive`函数。可以通过类似`sendBroadcast(Intent, String)`的接口在发送广播时指定接收者必须具备的`permission`或通过`Intent.setPackage`设置广播仅对某个程序有效。 + +- 保证我接收到的广播是指定对象发送过来的 + 当应用程序注册了某个广播时,即便设置了`IntentFilter`还是会接收到来自其他应用程序的广播进行匹配判断。对于动态注册的广播可以通过类似`registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)`的接口指定发送者必须具备的`permission`,对于静态注册的广播可以通过`android:exported="false"`属性表示接收者对外部应用程序不可用,即不接受来自外部的广播。 + +`android.support.v4.content.LocalBroadcastManager`工具类,可以实现在自己的进程内进行局部广播发送与注册,使用它比直接通过sendBroadcast(Intent)发送系统全局广播有以下几个好处: +- 因广播数据在本应用范围内传播,你不用担心隐私数据泄露的问题。 +- 不用担心别的应用伪造广播,造成安全隐患。 +- 相比在系统内发送全局广播,它更高效。 + +```java + LocalBroadcastManager mLocalBroadcastManager; + BroadcastReceiver mReceiver; + +@Override +protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + IntentFilter filter = new IntentFilter(); + filter.addAction("test"); + + mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals("test")) { + //Do Something + } + } + }; + mLocalBroadcastManager = LocalBroadcastManager.getInstance(this); + mLocalBroadcastManager.registerReceiver(mReceiver, filter); +} + + +@Override +protected void onDestroy() { + mLocalBroadcastManager.unregisterReceiver(mReceiver); + super.onDestroy(); +} +``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/AdavancedPart/Componse\344\275\277\347\224\250.md" "b/AdavancedPart/Componse\344\275\277\347\224\250.md" new file mode 100644 index 00000000..6a2f0449 --- /dev/null +++ "b/AdavancedPart/Componse\344\275\277\347\224\250.md" @@ -0,0 +1,130 @@ +Compose使用 +=== + +传统 Android UI 开发采用命令式模型,即通过命令驱动视图变化:findViewById 查找控件、设置属性、处理交互逻辑。代码经常伴随着多个职责耦合在一起,结构混乱,易错难测。 + +Compose 则采用声明式模型:界面即状态的函数表达。当状态改变时,对应的 Composable 自动重新组合(Recompose)并刷新界面。这种模式更贴近现代前端(如 React/Vue)的理念。 + +无需关心视图更新逻辑,只要状态变化,界面自然重绘,大幅降低 UI 层复杂度。 + + +1.3 开发效率提升点 + +代码量平均减少 40%-60% + +无需 ViewHolder、Adapter 逻辑 + +状态与 UI 同步更新,避免 UI 状态丢失 + +支持实时预览(@Preview)、热重载、即时调试 + + +我们在公司项目中构建了性能对比 Benchmark(测试设备为 Pixel 6、Android 13): + +2.1 滚动列表对比:RecyclerView vs LazyColumn + +指标 RecyclerView LazyColumn (Compose) 差异 +平均帧率 +48 fps +58 fps ++20% +内存占用 +28 MB +22 MB +-21% +首次绘制耗时 +320 ms +210 ms +-34% + + +2.2 原因解析:Compose 更快的秘密 + +SlotTable:结构快照树 +Compose 编译器会将 Composable 函数转换为组装 SlotTable 的代码。SlotTable 是一种高效的数据结构,存储了 Composable 树的结构快照。当状态发生变化时,Compose 通过对比 SlotTable 的版本,精确地定位变化范围,从而进行最小代价的重组操作(recomposition)。这一过程通过 Composer 对 Slot 表的操作实现,避免了冗余 UI 节点更新。 + +重组与 Group 管理机制 +Compose 使用 Group(startGroup/endGroup)对 Composable 调用进行打包与标识,每个重组区域会通过重新执行对应的 Group 来进行更新,确保仅变更部分被执行。此机制在 RecomposeScopeImpl 中有体现,它能追踪每个状态依赖的作用域,从而提升重组精度。 + +无需 ViewHolder 回收 +传统 RecyclerView 需要手动管理视图缓存与回收,而 Compose 自动处理 Composition 节点生命周期。Compose Compiler 会生成高效的 Slot 操作指令,通过“skip、reuse”策略对 UI 层进行精准控制,避免重复创建与销毁。 + +Skia 图形引擎与 RenderNode +Compose 绘制层基于 Skia 引擎,使用 DrawModifier 直接对 Canvas 进行渲染。它不会像传统 View 那样层层嵌套测量布局与绘制流程,而是采用测量(MeasurePass)-> 布局(LayoutPass)-> 绘制(DrawPass)的管线逻辑,通过 LayoutNode 驱动 Compose UI 树的变化。同时 Compose Layout 使用 SubcomposeLayout 实现异步测量能力,提高复杂嵌套组件的性能表现。 + +渲染流程对比 +阶段 View System Compose +布局树管理 +View/ViewGroup 层级 +LayoutNode 节点 +渲染方式 +Choreographer + RenderThread +FrameClock + Skia 渲染 +状态追踪 +手动触发 invalidate +Snapshot 自动追踪 + Diff Patch +更新路径 +requestLayout → measure/layout +Recomposer + SlotTable 重组 + +注意:Compose 并非所有场景都一定更快,特别是复杂嵌套、过度组合场景仍需谨慎使用。 + +### 简介 + +Jetpack Compose 是用于构建 Android 界面的新款工具包。 + + +Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。它可让您更快速、更轻松地构建 Android 界面。 + + +编写更少的代码会影响到所有开发阶段:作为代码撰写者,需要测试和调试的代码会更少,出现 bug 的可能性也更小,您就可以专注于解决手头的问题;作为审核人员或维护人员,您需要阅读、理解、审核和维护的代码就更少。 + +与使用 Android View 系统(按钮、列表或动画)相比,Compose 可让您使用更少的代码实现更多的功能。无论您需要构建什么内容,现在需要编写的代码都更少了。以下是我们的一些合作伙伴的感想: + +“对于相同的 Button 类,代码的体量要小 10 倍。”(Twitter) +“使用 RecyclerView 构建的任何屏幕(我们的大部分屏幕都使用它构建)的大小也显著减小。”(Monzo) +““只需要很少几行代码就可以在应用中创建列表或动画,这一点令我们非常满意。对于每项功能,我们编写的代码行更少了,这让我们能够将更多精力放在为客户提供价值上。”(Cuvva) +编写代码只需要采用 Kotlin,而不必拆分成 Kotlin 和 XML 部分:“当所有代码都使用同一种语言编写并且通常位于同一文件中(而不是在 Kotlin 和 XML 语言之间来回切换)时,跟踪变得更容易”(Monzo) + +无论您要构建什么,使用 Compose 编写的代码都很简洁且易于维护。“Compose 的布局系统在概念上更简单,因此可以更轻松地推断。查看复杂组件的代码也更轻松。”(Square) + + + +Compose 使用声明性 API,这意味着您只需描述界面,Compose 会负责完成其余工作。这类 API 十分直观 - 易于探索和使用:“我们的主题层更加直观,也更加清晰。我们能够在单个 Kotlin 文件中完成之前需要在多个 XML 文件中完成的任务,这些 XML 文件负责通过多个分层主题叠加层定义和分配属性。”(Twitter) + + +Compose 与您所有的现有代码兼容:您可以从 View 调用 Compose 代码,也可以从 Compose 调用 View。大多数常用库(如 Navigation、ViewModel 和 Kotlin 协程)都适用于 Compose,因此您可以随时随地开始采用。“我们集成 Compose 的初衷是实现互操作性,我们发现这件事情已经‘水到渠成’。我们不必考虑浅色模式和深色模式等问题,整个体验无比顺畅。”(Cuvva) + + +为现有应用设置 Compose + +首先,使用 Compose Compiler Gradle 插件配置 Compose 编译器。 + +然后,将以下定义添加到应用的 build.gradle 文件中: +``` +android { + buildFeatures { + compose = true + } +} +``` +在 Android BuildFeatures 代码块内将 compose 标志设置为 true 会在 Android Studio 中启用 Compose 功能。 + + +Compose vs HarmonyOS ArkUI 对比分析 + + +Jetpack Compose 和 HarmonyOS ArkUI 均采用声明式 UI 编程范式,面向多设备场景的响应式 UI 构建,二者在理念相通的同时,在架构设计、状态模型、渲染机制等方面有显著区别。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/compose_vs_arkui.png?raw=true) + + +Jetpack Compose 是围绕可组合函数构建的。这些函数可让您以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素、将其附加到父项等)。如需创建可组合函数,只需将 @Composable 注解添加到函数名称中即可。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" "b/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" new file mode 100644 index 00000000..f46d2dc2 --- /dev/null +++ "b/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" @@ -0,0 +1,233 @@ +ConstraintLaayout简介 +=== + +`ConstraintLayout`从发布到现在也得有两年的时间了,但是目前在项目中却很少用到他。今天闲下来记录一下,以后可以用来解决一些布局的嵌套问题。 +`ConstraintLayout`是`RelativeLayout`的升级版本,但是比`RelativeLayout`更加强调约束,它能让你的布局更加扁平化,一般来说一个界面一层就够了。 +而且它可以直接在布局编辑页面通过拖拖拖的方式就可以把控件摆放好,但是我还是习惯手写。 + +在`Android Studio`中可以很方便的将目前的布局直接转换成`ConstraintLayout`,可以在布局的编辑页面上直接右键然后选择就可以了。 + +当然项目中要添加它的依赖,目前最新版本: +``` +implementation 'com.android.support.constraint:constraint-layout:1.1.0' +``` + + + +然后会提示添加`ConstraintLayout`支持库。 + + + +相对于传统布局`ConstraintLayout`在以下方面提供了一些新的特性: + +- 相对定位 + + 这个和`RelativeLayout`比较像,就是一个控件相对于另一个控件的位置约束关系: + + - 横向:`Left、Right、Start、End` + - 纵向:`Top、Bottom、Baseline(文本底部的基准线)` + + ```xml + -``` - -或者 -``` - - - - - - -

点击按钮就可以执行 displayDate() 函数。

- - - - - -

- - - -``` - -- 创建和删除节点 -``` - - - - -
-

这是一个段落。

-

这是另一个段落。

-
- - - - - -``` - -- 删除节点 -``` - - - - -
-

这是一个段落。

-

这是另一个段落。

-
- - - - - - -``` - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - \ No newline at end of file diff --git "a/JS\345\237\272\347\241\200/JS\345\237\272\347\241\200\347\256\200\344\273\213\345\217\212\345\205\245\351\227\250.md" "b/JS\345\237\272\347\241\200/JS\345\237\272\347\241\200\347\256\200\344\273\213\345\217\212\345\205\245\351\227\250.md" deleted file mode 100644 index d141c950..00000000 --- "a/JS\345\237\272\347\241\200/JS\345\237\272\347\241\200\347\256\200\344\273\213\345\217\212\345\205\245\351\227\250.md" +++ /dev/null @@ -1,430 +0,0 @@ -JS基础简介及入门 -=== - -- JavaScript 是脚本语言 -- JavaScript 是一种轻量级的编程语言。 -- JavaScript 是可插入 HTML 页面的编程代码。 -- JavaScript 插入 HTML 页面后,可由所有的现代浏览器执行。 -- JavaScript 是大小写敏感的 - -废多看码: -- `HTML`中的脚本必须位于``标签之间。 -- 脚本可被放置在`HTML`页面的``和``部分中。 -- 通常的做法是把函数放入 部分中,或者放在页面底部。这样就可以把它们安置到同一处位置, - 不会干扰页面的内容。 - - -在`HTML`页面中嵌入`JavaScript`需要使用``来告诉`JavaScript`的 -开始和结束。之间的代码就是`JavaScript`: - -``` - -``` - -或者也可以把脚本保存到外部文件中。外部文件通常包含被多个网页使用的代码。扩展名是`.js`。 -如需使用外部文件,请在` - - - -``` - -**Note:**有些代码中` - -

My First JavaScript

-

Please input a number between 5 and 10:

- - -

- -``` - -- 对话框 - -三种弹出框:警告(alert)、确认(confirm)以及提问(prompt)。 -``` -alert("我是菜鸟我怕谁"); -``` - -``` -var r = confirm("你是菜鸟吗"); -if (r == true) -{ - document.write("彼此彼此"); -} else -{ - document.write("佩服佩服"); -} -``` - -`prompt`和`confirm`类似,不过它允许访客随意输入答案。 -``` -var score; -score = prompt("你的分数是多少?") -``` - -- 五种原始类型: - - - Undefined - - Null - - Boolean - - Number - - String - -- typeof运算符 - -``` -alert (typeof sTemp); //输出 "string" -alert (typeof 86); //输出 "number" -``` - -对变量或值调用 typeof 运算符将返回下列值之一: -- undefined - 如果变量是 Undefined 类型的 -- boolean - 如果变量是 Boolean 类型的 -- number - 如果变量是 Number 类型的 -- string - 如果变量是 String 类型的 -- object - 如果变量是一种引用类型或 Null 类型的 - - -- instanceof 运算符 -在使用`typeof`运算符时采用引用类型存储值会出现一个问题,无论引用的是什么类型的对象, -它都返回`object`。引入了另一个`Java`运算符`instanceof`来解决这个问题。 - - -最后看下这个小`demo`,为下面的`DOM`作铺垫。 -``` - - - - -

我的第一段 JavaScript

- -

-JavaScript 能改变 HTML 元素的内容。 -

- - - - - - - -``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - \ No newline at end of file diff --git "a/JS\345\237\272\347\241\200/README.md" "b/JS\345\237\272\347\241\200/README.md" deleted file mode 100644 index 46e58dfb..00000000 --- "a/JS\345\237\272\347\241\200/README.md" +++ /dev/null @@ -1,6 +0,0 @@ -JS学习笔记 -=== - -公司中项目都是js与android的调用,功能在android上很容易去实现,但是用到公司的js互调之后那简直寸步难行。完全看不懂稍高级点的js代码,之前对js -有简单的了解现在也都忘记了,没办法,只能重新学下js。 -虽然一直对js不感兴趣,总想着学ios、go,但是毕竟工作任务还是要按时完成的,硬着头皮学吧。 diff --git "a/JS\345\237\272\347\241\200/Window.md" "b/JS\345\237\272\347\241\200/Window.md" deleted file mode 100644 index 95820f17..00000000 --- "a/JS\345\237\272\347\241\200/Window.md" +++ /dev/null @@ -1,135 +0,0 @@ -Window -=== - -所有浏览器都支持`window`对象。它表示浏览器窗口。 -所有`JavaScript`全局对象、函数以及变量均自动成为`window`对象的成员。 -全局变量是`window`对象的属性。 -全局函数是`window`对象的方法。 -甚至`HTML DOM`的`document也是`window`对象的属性之一: -如 -``` -window.document.getElementById("header"); -``` -``` -window.innerHeight - 浏览器窗口的内部高度 -window.innerWidth - 浏览器窗口的内部宽度 -window.open() - 打开新窗口 -window.close() - 关闭当前窗口 -window.moveTo() - 移动当前窗口 -window.resizeTo() - 调整当前窗口的尺寸 -``` - -- Window Screen - -`window.screen`对象在编写时可以不使用`window`这个前缀。 -一些属性: -``` -screen.availWidth - 可用的屏幕宽度 -screen.availHeight - 可用的屏幕高度 -``` - -- Window Location -`window.location`对象在编写时可不使用`window`这个前缀。 -``` -location.hostname 返回 web 主机的域名 -location.pathname 返回当前页面的路径和文件名 -location.port 返回 web 主机的端口 (80 或 443) -location.protocol 返回所使用的 web 协议(http:// 或 https://) -location.href 属性返回当前页面的 URL。 -location.pathname 属性返回 URL 的路径名。 -location.assign() 方法加载新的文档。 -``` - --Window History -`window.history`对象在编写时可不使用`window`这个前缀。 -``` -history.back() - 与在浏览器点击后退按钮相同 -history.forward() - 与在浏览器中点击按钮向前相同 -``` - --Window Navigator -`window.navigator`对象在编写时可不使用`window`这个前缀。 -代表正在使用的浏览器对象 -``` - -``` - -- 计时 - -``` -var t=setTimeout("javascript语句",毫秒) -``` -`setTimeout()`方法会返回某个值。在上面的语句中,值被储存在名为`t`的变量中。 -假如你希望取消这个`setTimeout()`,你可以使用这个变量名来指定它。 -``` -clearTimeout(setTimeout_variable)// 取消 -``` -如: -``` - - - - - - - -
- - - -
- - - -``` - - -- 使用`jQuery`框架库 -为了引用某个库,请使用` - - - - - -``` - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - \ No newline at end of file diff --git "a/JS\345\237\272\347\241\200/\345\257\271\350\261\241.md" "b/JS\345\237\272\347\241\200/\345\257\271\350\261\241.md" deleted file mode 100644 index 4d7cc31d..00000000 --- "a/JS\345\237\272\347\241\200/\345\257\271\350\261\241.md" +++ /dev/null @@ -1,89 +0,0 @@ -对象 -=== - -- 创建对象 -``` -person=new Object(); -person.firstname="Bill"; -person.lastname="Gates"; -person.age=56; -person.eyecolor="blue"; -``` -或者 -``` -person={firstname:"John",lastname:"Doe",age:50,eyecolor:"blue"}; -``` - -- 构造函数 -``` -function person(firstname,lastname,age,eyecolor) -{ - this.firstname=firstname; - this.lastname=lastname; - this.age=age; - this.eyecolor=eyecolor; -} -``` -通过构造函数创建对象 -``` -var myFather=new person("Bill","Gates",56,"blue"); -var myMother=new person("Steve","Jobs",48,"green"); -``` - -- 类 - -`JavaScript`是面向对象的语言,但`JavaScript`不使用类。 -在`JavaScript`中,不会创建类,也不会通过类来创建对象(就像在其他面向对象的语言中那样)。 -`JavaScript`基于`prototype`,而不是基于类的。 - -- Number对象 -``` -var myNum=new Number(value); -var myNum=Number(value); -``` - -- String对象 - -- Date对象 -``` -var myDate=new Date(); -myDate.setFullYear(2008,8,9); - -var today = new Date(); - -if (myDate>today) -{ -alert("Today is before 9th August 2008"); -} -else -{ -alert("Today is after 9th August 2008"); -} -``` - -- Array对象 -``` -var mycars=new Array() -mycars[0]="Saab" -mycars[1]="Volvo" -mycars[2]="BMW" -``` -或 -``` -var mycars=new Array("Saab","Volvo","BMW") -``` - -- Boolean对象 -- Math对象 -``` -document.write(Math.round(4.7)) -``` - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/Base64\345\212\240\345\257\206.md" "b/JavaKnowledge/Base64\345\212\240\345\257\206.md" similarity index 90% rename from "Java\345\237\272\347\241\200/Base64\345\212\240\345\257\206.md" rename to "JavaKnowledge/Base64\345\212\240\345\257\206.md" index 81a600f9..1dbe14c9 100644 --- "a/Java\345\237\272\347\241\200/Base64\345\212\240\345\257\206.md" +++ "b/JavaKnowledge/Base64\345\212\240\345\257\206.md" @@ -29,9 +29,7 @@ Base64加密 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/base64_man.png?raw=true) -正式因为这,所以 -想要转换成`Base64`最少要三个字节才可以,转换出来的`Base64`最少是4个字节,但是如果我要转换的字节不够3个怎么办?比如我想对字符`A`进行`Base64` -加密。,`A`对应的第二个`Base64`的二进制位只有两个,把后边的四个补0就是了。 +正是因为这,所以想要转换成`Base64`最少要三个字节才可以,转换出来的`Base64`最少是4个字节,但是如果我要转换的字节不够3个怎么办?比如我想对字符`A`进行`Base64`加密。,`A`对应的第二个`Base64`的二进制位只有两个,把后边的四个补0就是了。 所以`A`对应的`Base64`字符就是QQ。上边已经说过了,原则是`Base64`字符的最小单位是四个字符一组,那这才两个字符,后边补两个"="吧。 其实不用"="也不耽误解码,之所以用"=",可能是考虑到多段编码后的Base64字符串拼起来也不会引起混淆。 由此可见 Base64字符串只可能最后出现一个或两个"=",中间是不可能出现"="的。下图中字符"BC"的编码过程也是一样的。 @@ -50,4 +48,4 @@ Base64加密 - 邮箱 :charon.chui@gmail.com - Good Luck! - \ No newline at end of file + diff --git "a/JavaKnowledge/Git\347\256\200\344\273\213.md" "b/JavaKnowledge/Git\347\256\200\344\273\213.md" new file mode 100644 index 00000000..2d427891 --- /dev/null +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -0,0 +1,791 @@ +Git简介 +======= + +`Git`和其他版本控制系统(包括`Subversion`及其他相似的工具)的主要差别在于`Git`对待数据的方法。概念上来区分,其它大部分系统以文件 +变更列表的方式存储信息。 +这类系统(`CVS、Subversion、Perforce、Bazaar`等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。 +存储每个文件与初始版本的差异。 + +`Git`不按照以上方式对待或保存数据。反之,`Git`更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在`Git`中保存项目 +状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,`Git`不再重新存储该文件,而是只保留一个 +链接指向之前存储的文件。`Git`对待数据更像是一个快照流。 + +`Git`是分布式版本控制系统,集中式和分布式版本控制有什么区别呢? + +- 集中式版本控制系统 + 版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了, + 再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了, + 再放回图书馆。集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上, + 遇到网速慢的话,可能提交一个10M的文件就需要5分钟,这还不得把人给憋死啊。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_jizhong.jpeg) +- 分布式版本控制系统 + 分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在 + 你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上 + 改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。 + 和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧, + 随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。 + 在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相 + 访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器 + 的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_fenbu.jpeg) + +版本库 +------ + +什么是版本库呢?版本库又名仓库,英文名`repository`,你可以简单理解成一个目录,这个目录里面的所有文件都可以被`Git`管理起来, +每个文件的修改、删除,`Git`都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。 + +所以,创建一个版本库非常简单: + +- 创建一个空目录 +- 通过`git init`命令把这个目录变成`Git`可以管理的仓库 + 瞬间`Git`就把仓库建好了,而且告诉你是一个空的仓库`(empty Git repository)`,细心的读者可以发现当前目录下多了一个`.git`的目录, + 这个目录是`Git`来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把`Git`仓库给破坏了。 +- 使用命令`git add `,注意,可反复多次使用,添加多个文件; +- 使用命令`git commit`,完成。 + +Git的五种状态 +------------- + +`Git`有五种状态,你的文件可能处于其中之一: + +- 未修改`(origin)` +- 已修改`(modified)` +- 已暂存`(staged)` +- 已提交`(committed)` +- 已推送`(pushed)` + +已提交表示数据已经安全的保存在本地数据库中。 +已修改表示修改了文件,但还没保存到数据库中。 +已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。 + +`Git`仓库目录是`Git`用来保存项目的元数据和对象数据库的地方。这是`Git`中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。 +工作目录是对项目的某个版本独立提取出来的内容。这些从`Git`仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。 + +暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在`Git`仓库目录中。 有时候也被称作‘索引’,不过一般说法还是叫暂存区域。 + +基本的`Git`工作流程如下: + +- 在工作目录中修改文件。 +- 暂存文件,将文件的快照放入暂存区域。 +- 提交更新,找到暂存区域的文件,将快照永久性存储到`Git`仓库目录。 + +四个区 +------ + +`Git`主要分为四个区: + +- 工作区`(Working Area)` +- 暂存区`(Stage或Index Area)` +- 本地仓库`(Local Repository)` +- 远程仓库`(Remote Repository)` + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_buzhou.jpg) + +正常情况下,我们的工作流程就是三个步骤,分别对应上图中的三个箭头线: + +```shell +git add . // 把所有文件放入暂存区 +git commit -m "comment" // 把所有文件从暂存区提交进本地仓库 +git push // 把所有文件从本地仓库推送进远程仓库 +``` + +先上一张图 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.png) + +图中的`index`部分就是暂存区 + +## 三棵树 + +Git作为一个系统,是以它的一般操作来管理并操纵这三棵树的: + +| 树 | 用途 | +| :---------------- | :----------------------------------- | +| HEAD | 上一次提交的快照,下一次提交的父结点 | +| Index | 预期的下一次提交的快照 | +| Working Directory | 沙盒 | + +#### HEAD + +HEAD是当前分支引用的指针,它总是指向该分支上的最后一次提交。这表示HEAD将是下一次提交的父结点。通常,理解HEAD的最简方式, +就是将它看做**该分支上的最后一次提交**的快照。 + +#### 索引 + +索引是你的**预期的下一次提交**。我们也会将这个概念引用为Git的“暂存区”,这就是当你运行`git commit`时Git看起来的样子。 + +#### 工作目录 + +最后,你就有了自己的**工作目录**(通常也叫**工作区**)。 另外两棵树以一种高效但并不直观的方式,将它们的内容存储在`.git`文件夹中。 +工作目录会将它们解包为实际的文件以便编辑。你可以把工作目录当做**沙盒**。在你将修改提交到暂存区并记录到历史之前,可以随意更改。 + +#### Git目录下文件的状态: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_file_lifecycle.png?raw=true) +你工作目录下的每一个文件都不外乎这两种状态: + +- 已跟踪(Tracked) + 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有他们的记录,在工作一段时间后,它们的状态可能是未修改,已修改或 + 已放入暂存区。简而言之,已跟踪的文件就是Git已经知道的文件 +- 未跟踪(Untracked) + 工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们即不存在与上次快照的记录中,也没有被放入暂存区。 + +## 常用命令 + +### git config + +安装好git后我们要先配置一下。以便`git`跟踪。 +``` + git config --global user.name "xxx" + git config --global user.email "xxx@xxx.com" +``` +上面修改后可以使用`cat ~/.gitconfig`查看 +如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 +`git config --list`来查看所有的配置。 +如果需要查看当前的user.name和user.email的值可以通过`git config user.name` + +### git init + +新建仓库 +``` +mkdir gitDemo +cd gitDemo +git init +``` +这样就创建完了。 + +### git clone仓库 + +在某一目录下执行. +`git clone [git path]` +执行后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 + +### git add提交文件更改(修改和新增),把当前的修改添加到暂存区 + +`git add xxx.txt`添加某一个文件 +`git add .`添加当前目录所有的文件 + +### `git commit`提交,把修改由暂存区提交到仓库中 + +`git commit`提交,然后在出来的提示框内查看当前提交的内容以及输入注释。 +或者也可以用`git commit -m "xxx"` 提交到本地仓库并且注释是xxx + +`git commit`是很小的一件事情,但是往往小的事情往往引不起大家的关注,不妨打开公司的任一个`repo`,查看`commit log`, +满篇的`update`和`fix`,完全不知道这些`commit`是要做啥。在提交`commit`的时候尽量保证这个`commit`只做一件事情, +比如实现某个功能或者修改了配置文件。注意是保证每个`commit`只做一件事,而不是让你做了一件事`commit`后就`push`, +那样就有点过分了。 + +### `git cherry-pick` + +`git cherry-pick`可以选择某一个分支中的一个或几个`commit(s)`来进行操作。例如,假设我们有个稳定版本的分支,叫`v2.0`, +另外还有个开发版本的分支`v3.0`,我们不能直接把两个分支合并,这样会导致稳定版本混乱,但是又想增加一个`v3.0`中的功能到`v2.0`中, +这里就可以使用`cherry-pick`了。 +就是对已经存在的`commit`进行 再次提交; +简单用法: +`git cherry-pick ` + +`git rebase`命令基本是是一个自动化的`cherry-pick`命令。它计算出一系列的提交,然后再以它们在其他地方以同样的顺序一个一个的`cherry-picks`出它们。 + +### `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 + +### `git diff` + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_diff.webp?raw=true) +`git diff`直接查看当前修改未add(暂存staged)的差别 +`git diff --staged`查看已add(到暂存区)的差别`git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 + +- 首先如果我们只是本地修改了一个文件,但是还没有执行`git add .`之前,该如何查看有那些修改。这种情况下直接执行`git diff`就可以了。 +- 那如果我们执行了`git add .`操作,然后你再执行`git diff`这时就会发现没有任何结果,这时因为`git diff`这个命令只是检查工作区和暂存区之间的差异。 + 如果我们要查看暂存区和本地仓库之间的差异就需要加一个参数使用`--staged`参数或者`--cached`,`git diff --cached`。这样再执行就可以看到暂存区和本地仓库之间的差异。 +- 现在如果我们把修改使用`git commit`从暂存区提交到本地仓库,再看一下差异。这时候再执行`git diff --cached`就会发现没有任何差异。 + 如果我们行查看本地仓库和远程仓库的差异,就要换另一个参数,执行`git diff master origin/master`这样就可以看到差异了。 这里面`master`是本地的仓库,而`origin/master`是远程仓库,因为默认都是在主分支上工作,所以两边都是`master`而`origin`代表远程。 + +### `git push` 提交到远程仓库 + +可以直接调用`git push`推送到当前分支 +或者`git push origin master`推送到远程`master`分支 +`git push origin devBranch`推送到远程`devBranch`分支 +### `git log`查看当前分支下的提交记录 + +用`git log`可以查看提交历史,以便确定要回退到哪个版本。 +如果已经使用`git log`查出版本`commit id`后`reset`到某一次提交后,又要重返回来,用`git reflog`查看命令历史,以便确定要回到未来的哪个版本。 + +``` +git log -p -2 // -p 是仅显示最近的x次提交 +git log --stat // stat简略的显示每次提交的内容梗概,如哪些文件变更,多少删除,多少添加 +git log --oneline --graph +git log --grep="1" +``` +下面是常用的参数: + +- `–-author=“Alex Kras”` ——只显示某个用户的提交任务 +- `–-name-only` ——只显示变更文件的名称 +- `–-oneline`——将提交信息压缩到一行显示 +- `–-graph` ——显示所有提交的依赖树 +- `–-reverse` ——按照逆序显示提交记录(最先提交的在最前面) +- `–-after` ——显示某个日期之后发生的提交 +- `–-before` ——显示发生某个日期之前的提交 +- `--grep` ——过滤内容 + +#### Git日志搜索 + +如果你想知道某一个东西是什么时候存在或者引入的。git log命令有许多强大的工具可以通过提交信息甚至是diff的内容来找到某个特定的提交。 +例如,如果我们想找到ZLIB_BUF_MAX常量是什么时候引入的,我们可以使用-S选项来显示新增和删除该字符串的提交: + +```shell +git log -S ZLIB_BUF_MAX --oneline +``` +### `git reflog` + +可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 + +### `git reset` + +`git reset`命令用于将当前HEAD复位到指定状态。一般用于撤消之前的一些操作(如:`git add`,`git commit`等)。 +在`git`的一般使用中,如果发现错误的将不想暂存的文件被`git add`进入索引之后,想回退取消,则可以使用命令:`git reset HEAD `, +同时`git add`完毕之后,`git`也会做相应的提示,比如: + +```shell +# Changes to be committed: +# (use "git reset HEAD ..." to unstage) +# +# new file: test.py +``` +`git reset [--hard|soft|mixed|merge|keep] [或HEAD]`:将当前的分支重设`(reset)`到指定的``或者`HEAD`(默认,如果不显示指定``,默认是`HEAD`,即最新的一次提交),并且根据`[mode]`有可能更新索引和工作目录。`mode`的取值可以是`hard、soft、mixed、merged、keep`。下面来详细说明每种模式的意义和效果: + +- `--hard`:彻底回退到某一个版本,本地的源码也会变为上一个版本的内容。重删除工作空间改动代码,撤销commit,撤销git add .。所有变更集都会被丢弃。 +- `--mixed`:默认方式,它回退到某个版本,只保留源码,不删除工作空间改动代码,撤销commit,并且撤销git add . 。所有变更集都放在工作区。 +- `--soft`: 回退到某个版本,不删除工作空间改动代码,撤销commit,不撤销git add . ,所有变更集都放在暂存区,如果还要提交直接重新commit即可。 + +#### 示例 + +假设我们进入到一个新目录,其中有一个文件。 我们称其为该文件的v1版本,将它标记为蓝色。 +现在运行git init,这会创建一个Git仓库,其中的HEAD引用指向未创建的master分支。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex1.png?raw=true) +此时,只有工作目录有内容。 +现在我们想要提交这个文件,所以用git add来获取工作目录中的内容,并将其复制到索引中。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex2.png?raw=true) +接着运行git commit,它会取得索引中的内容并将它保存为一个永久的快照,然后创建一个指向该快照的提交对象,最后更新master来指向本次提交。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex3.png?raw=true) +此时如果我们运行 git status,会发现没有任何改动,因为现在三棵树完全相同。 + +现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的v2版本,并将它标记为红色。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex4.png?raw=true) +如果现在运行git status,我们会看到文件显示在 “Changes not staged for commit” 下面并被标记为红色,因为该条目在索引与工作目录之间存在不同。 +接着我们运行git add来将它暂存到索引中。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex5.png?raw=true) +此时,由于索引和HEAD不同,若运行git status的话就会看到“Changes to be committed” 下的该文件变为绿色 ——也就是说,现在预期的下一次提交 +与上一次提交不同。 最后,我们运行git commit来完成提交。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex6.png?raw=true) +现在运行git status会没有输出,因为三棵树又变得相同了。 + +切换分支或克隆的过程也类似。当检出一个分支时,它会修改HEAD指向新的分支引用,将索引填充为该次提交的快照, +然后将索引的内容复制到工作目录中。 + +#### 重置的作用 + +在以下情景中观察reset命令会更有意义。 + +为了演示这些例子,假设我们再次修改了file.txt 文件并第三次提交它。 现在的历史看起来是这样的: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-start.png?raw=true) +让我们跟着reset看看它都做了什么。它以一种简单可预见的方式直接操纵这三棵树。它做了三个基本操作。 + +##### 第 1 步:移动 HEAD + +reset 做的第一件事是移动 HEAD 的指向。 这与改变 HEAD 自身不同(checkout 所做的);reset 移动 HEAD 指向的分支。 这意味着如果 HEAD 设置为 master 分支(例如,你正在 master 分支上), 运行 git reset 9e5e6a4 将会使 master 指向 9e5e6a4。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-soft.png?raw=true) +无论你调用了何种形式的带有一个提交的 reset,它首先都会尝试这样做。 使用 reset --soft,它将仅仅停在那儿。 + +现在看一眼上图,理解一下发生的事情:它本质上是撤销了上一次 git commit 命令。 当你在运行 git commit 时,Git 会创建一个新的提交,并移动 HEAD 所指向的分支来使其指向该提交。 当你将它 reset 回 HEAD~(HEAD 的父结点)时,其实就是把该分支移动回原来的位置,而不会改变索引和工作目录。 现在你可以更新索引并再次运行 git commit 来完成 git commit --amend 所要做的事情了 + +##### 第 2 步:更新索引(--mixed) + +注意,如果你现在运行 git status 的话,就会看到新的 HEAD 和以绿色标出的它和索引之间的区别。 + +接下来,reset 会用 HEAD 指向的当前快照的内容来更新索引。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-mixed.png?raw=true) +如果指定 --mixed 选项,reset 将会在这时停止。 这也是默认行为,所以如果没有指定任何选项(在本例中只是 git reset HEAD~),这就是命令将会停止的地方。 + +现在再看一眼上图,理解一下发生的事情:它依然会撤销一上次 提交,但还会 取消暂存 所有的东西。 于是,我们回滚到了所有 git add 和 git commit 的命令执行之前。 + +##### 第 3 步:更新工作目录(--hard) + +reset 要做的的第三件事情就是让工作目录看起来像索引。 如果使用 --hard 选项,它将会继续这一步。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-hard.png?raw=true) +现在让我们回想一下刚才发生的事情。 你撤销了最后的提交、git add 和 git commit 命令 以及 工作目录中的所有工作。 + +必须注意,--hard 标记是 reset 命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset 调用都可以轻松撤消,但是 --hard 选项不能,因为它强制覆盖了工作目录中的文件。 在这种特殊情况下,我们的 Git 数据库中的一个提交内还留有该文件的 v3 版本, 我们可以通过 reflog 来找回它。但是若该文件还未提交,Git 仍会覆盖它从而导致无法恢复。 + +回顾reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止: + +- 移动 HEAD 分支的指向 (若指定了 --soft,则到此停止) +- 使索引看起来像 HEAD (若未指定 --hard,则到此停止) +- 使工作目录看起来像索引 + +##### 通过路径来重置 + +前面讲述了 reset 基本形式的行为,不过你还可以给它提供一个作用路径。 若指定了一个路径,reset 将会跳过第 1 步,并且将它的作用范围限定为指定的文件或文件集合。 这样做自然有它的道理,因为 HEAD 只是一个指针,你无法让它同时指向两个提交中各自的一部分。 不过索引和工作目录 可以部分更新,所以重置会继续进行第 2、3 步。 + +现在,假如我们运行 git reset file.txt (这其实是 git reset --mixed HEAD file.txt 的简写形式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 --soft 或 --hard),它会: + +- 移动 HEAD 分支的指向 (已跳过) +- 让索引看起来像 HEAD (到此处停止) + +所以它本质上只是将 file.txt 从 HEAD 复制到索引中。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path1.png?raw=true) +它还有 取消暂存文件 的实际效果。 如果我们查看该命令的示意图,然后再想想 git add 所做的事,就会发现它们正好相反。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path2.png?raw=true) +这就是为什么 git status 命令的输出会建议运行此命令来取消暂存一个文件。我们可以不让 Git 从 HEAD 拉取数据,而是通过具体指定一个提交来拉取该文件的对应版本。 我们只需运行类似于 git reset eb43bf file.txt 的命令即可。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path3.png?raw=true) +它其实做了同样的事情,也就是把工作目录中的文件恢复到 v1 版本,运行 git add 添加它, 然后再将它恢复到 v3 版本(只是不用真的过一遍这些步骤)。 如果我们现在运行 git commit,它就会记录一条“将该文件恢复到 v1 版本”的更改, 尽管我们并未在工作目录中真正地再次拥有它。 + +还有一点同 git add 一样,就是 reset 命令也可以接受一个 --patch 选项来一块一块地取消暂存的内容。 这样你就可以根据选择来取消暂存或恢复内容了。 + +### `git checkout`撤销修改或者切换分支 + +`git checkout -- xx.txt`意思就是将`xx.txt`文件在工作区的修改全部撤销。可能会有两种情况: + +- 修改后还没有调用`git add`添加到暂存区,现在撤销后就会和版本库一样的状态。 +- 修改后已经调用`git add`添加到暂存区后又做了修改,这时候撤销就会回到暂存区的状态。 + +总的来说`git checkout`就是让这个文件回到最近一次`git commit`或者`git add`的状态。 +这里还有一个问题就是我胡乱修改了某个文件内容然后调用了`git add`添加到缓存区中,这时候想丢弃修改该怎么办?也是要分两步: + +- 使用`git reset HEAD file`命令,将暂存区中的内容回退,这样修改的内容会从暂存区回到工作区。 +- 使用`git checkout --file`直接丢弃工作区的修改。 + +`git checkout`把当前目录所有修改的文件从`HEAD`都撤销修改。 +为什么分支的地方也是用`git checkout`这里撤销还是用它呢?他们的区别在于`--`,如果没有`--`那就是检出分支了。 +`git checkout origin/developer` // 切换到orgin/developer分支 + +上面介绍了两个回退操作`git reset`和`git checkout`,这里就总结一下如何来对修改进行撤销操作: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_reset_checkout.png?raw=true) + +- 已经修改,但是并未执行`git add .`进行暂存 + 如果只是修改了本地文件,但是还没有执行`git add .`这时候我们的修改还是在工作区,并未进入暂存区,我们可以使用:`git checkouot .`或者`git reset --hard`来进行撤销操作。 + + `git add .`的反义词是`git checkout .`做完修改后,如果想要向前一步,让修改进入暂存区执行`git add .`如果想退后一步,撤销修改就执行`git checkout .`。 +- 已暂存,未提交 + 如果已经执行了`git add .`但是还没有执行`git commit -m "comment"`这时候你意识到了错误,想要撤销,可以执行: + + ``` + git reset // git reset 只是把修改退回到了git add .之前的状态,也就是让文件还处于已修改未暂存的状态 + git checkout . // 上面让文件处于已修改未暂存的状态,还要执行git checkout .来撤销工作区的状态 + ``` + 或`git reset --hard` + + 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 +- 已提交,未推送 + 如果执行了`git add .`又执行了`git commit -m "comment"`提交了代码,这时候代码已经进入到了本地仓库,然而你发现问题了,想要撤销,怎么办? + 执行`git reset --hard origin/master`还是`git reset --hard`命令,只不过这次多了一个参数`origin/master`,这代表远程仓库,既然本地仓库已经有了 + 你提交的脏代码,那么就从远程仓库中把代码恢复把。 + + 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? + `git reset --soft HEAD^`,这样就成功的撤销了你的commit。注意,仅仅是撤回commit操作,您写的代码仍然保留。 +- 已推送到远程仓库 + 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 + + ``` + git reset --hard HEAD^ // HEAD^代表最新提交的前一次 + git push -f // 强制推送 + ``` + +### reset checkout的区别 + +你大概还想知道 checkout 和 reset 之间的区别。 和 reset 一样,checkout 也操纵三棵树,不过它有一点不同,这取决于你是否传给该命令一个文件路径。 + +不带路径运行 git checkout [branch] 与运行 git reset --hard [branch] 非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别。 + +首先不同于 reset --hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。 其实它还更聪明一些。它会在工作目录中先试着 +简单合并一下,这样所有还未修改过的 文件都会被更新。而 reset --hard 则会不做检查就全面地替换所有东西。 + +第二个重要的区别是 checkout 如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。 + +例如,假设我们有 master 和 develop 分支,它们分别指向不同的提交;我们现在在 develop 上(所以 HEAD 指向它)。 +如果我们运行 git reset master,那么 develop 自身现在会和 master 指向同一个提交。 而如果我们运行 git checkout master 的话, +develop 不会移动,HEAD 自身会移动。 现在 HEAD 将会指向 master。 + +所以,虽然在这两种情况下我们都移动 HEAD 使其指向了提交 A,但做法是非常不同的。 reset 会移动 HEAD 分支的指向,而checkout则移动HEAD自身。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-checkout.png?raw=true) + +#### 带路径 + +运行 checkout 的另一种方式就是指定一个文件路径,这会像 reset 一样不会移动 HEAD。 它就像 git reset [branch] file 那样用该次提交中的那个文件来更新索引,但是它也会覆盖工作目录中对应的文件。 它就像是 git reset --hard [branch] file(如果 reset 允许你这样运行的话), 这样对工作目录并不安全,它也不会移动 HEAD。 + +此外,同 git reset 和 git add 一样,checkout 也接受一个 --patch 选项,允许你根据选择一块一块地恢复文件内容。 + +### `git revert`撤销提交 + +`git revert`在撤销一个提交的同时会创建一个新的提交,这是一个安全的方法,因为它不会重写提交历史。 + +- `git revert`是生成一个新的提交来撤销某次提交,此次提交之前的`commit`都会被保留 +- `git reset`是回到某次提交,提交及之前的`commit`都会被保留,但是此次之后的修改都会被退回到暂存区 + +相比`git reset`它不会改变现在得提交历史。`git reset`是直接删除指定的`commit`并把`HEAD`向后移动了一下。而`git revert`是一次新的特殊的`commit`,`HEAD`继续前进,本质和普通`add commit`一样,仅仅是`commit`内容很特殊。内容是与前面普通`commit`变化的反操作。 +比如前面普通`commit`是增加一行`a`,那么`revert`内容就是删除一行`a`。 +在 Git 开发中通常会控制主干分支的质量,但有时还是会把错误的代码合入到远程主干。虽然可以直接回滚远程分支,但有时新的代码也已经合入, +直接回滚后最近的提交都要重新操作。 那么有没有只移除某些Commit的方式呢?可以用一次revert操作来完成。 + +考虑这个例子,我们提交了 6 个版本,其中 3-4 包含了错误的代码需要被回滚掉。 同时希望不影响到后续的 5-6。 + +```shell +* 982d4f6 (HEAD -> master) version 6 +* 54cc9dc version 5 +* 551c408 version 4, harttle screwed it up again +* 7e345c9 version 3, harttle screwed it up +* f7742cd version 2 +* 6c4db3f version 1 +``` +这种情况在团队协作的开发中会很常见:可能是流程或认为原因不小心合入了错误的代码,也可能是合入一段时间后才发现存在问题。 +总之已经存在后续提交,使得直接回滚不太现实。 + +下面的部分就开始介绍具体操作了,同时我们假设远程分支是受保护的(不允许 Force Push)。思路是从产生一个新的 Commit 撤销之前的错误提交。 + +使用 git revert 可以撤销指定的提交, 要撤销一串提交可以用 .. 语法。 注意这是一个前开后闭区间, +即不包括commit1,但包括commit2。 + +```shell +git revert --no-commit f7742cd..551c408 +git commit -a -m 'This reverts commit 7e345c9 and 551c408' +``` +其中 f7742cd 是 version 2,551c408 是 version 4,这样被移除的是 version 3 和 version 4。 +注意 revert 命令会对每个撤销的 commit 进行一次提交,--no-commit 后可以最后一起手动提交。 + +此时 Git 记录是这样的: + +```shell +* 8fef80a (HEAD -> master) This reverts commit 7e345c9 and 551c408 +* 982d4f6 version 6 +* 54cc9dc version 5 +* 551c408 version 4, harttle screwed it up again +* 7e345c9 version 3, harttle screwed it up +* f7742cd version 2 +* 6c4db3f version 1 +``` +现在的 HEAD(8fef80a)就是我们想要的版本,把它 Push 到远程即可。 + +git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。 + +### `git rm`删除文件 + +该文件就不再纳入版本管理了。如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母),以防误删除文件后丢失修改的内容。 +另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。 +比如一些大型日志文件或者一堆 .a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上, +用 --cached 选项即可:`git rm --cached readme.txt` + +### 分支 + +`git`分支的创建和合并都是非常快的,因为增加一个分支其实就是增加一个指针,合并其实就是让某个分支的指针指向某一个位置。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_master_branch.png?raw=true) + +#### 创建分支 + +`git branch devBranch`创建名为`devBranch`的分支。 +`git checkout devBranch`切换到`devBranch`分支。 +`git checkout -b devBranch`创建+切换到分支`devBranch`。 +`git branch`查看当前仓库中的分支。 +`git branch -r`查看远程仓库的分支。 +`git branch -d devBranch`删除`devBranch`分支。 + +``` +origin/HEAD -> origin/master +origin/developer +origin/developer_sg +origin/master +origin/master_sg +origin/offline +``` +`git branch -d devBranch`删除`devBranch`分支。 +当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有被合并,删除失败。 +这时如果你要强行删除的话可以使用命令`git branch -D xxx`. +如何删除远程分支呢? + +``` +git branch -r -d origin/developer +git push origin :developer +``` +如何本地创建分支并推送给远程仓库? + +``` +// 本地创建分支 +git checkout master //进入master分支 +git checkout -b frommaster //以master为源创建分支frommaster +// 推送到远程仓库 +git push origin frommaster// 推送到远程仓库所要使用的名字 +``` +如何切到到远程仓库分支进行开发呢? +`git checkout -b frommaster origin/frommaster` +// 本地新建frommaster分支并且与远程仓库的frommaster分支想关联 +提交更改的话就用 +`git push origin frommaster` + +// 重命名分支 +`git branch -m new_branch wchar_support` +// 查看每一个分支的最后一次提交 +`git branch -v` + +#### `git merge`合并指定分支到当前分支 + +`git merge devBranch`将`devBranch`分支合并到`master`。 + +### 打`tag` + +`git tag v1.0`来进行打`tag`,默认为`HEAD` +`git tag`查看所有`tag` +如果我想在之前提交的某次`commit`上打`tag`,`git tag v1.0 commitID` +当然也可以在打`tag`时带上参数 `git tag v1.0 -m "version 1.0 released" commitID` +`git tag -d xxx`删除xxx + +`git show tagName`来查看某`tag`的详细信息。 + +- 打完`tag`后怎么推送到远程仓库 + `git push origin tagName` +- 删除`tag` + `git tag -d tagName` +- 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 + `git push origin:refs/tags/tagName` +- 忽略文件 + 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 + +### amend修改最后一次提交 + +有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以修改后重新git add 然后使用`--amend`选项重新提交:`git commit --amend`,然后再执行`git push`操作。 + +修补后的提交可能需要修补提交信息,当你在修补一次提交时,可以同时修改提交信息和提交内容。 如果你修补了提交的内容,那么几乎肯定要更新提交消息以反映修改后的内容。 + +另一方面,如果你的修补是琐碎的(如修改了一个笔误或添加了一个忘记暂存的文件), 那么之前的提交信息不必修改,你只需作出更改,暂存它们,然后通过以下命令避免不必要的编辑器环节即可: + +$ git commit --amend --no-edit + +#### 修改多个提交信息 + +为了修改在提交历史中较远的提交,必须使用更复杂的工具。 Git 没有一个改变历史工具,但是可以使用变基工具来变基一系列提交,基于它们原来的 HEAD 而不是将其移动到另一个新的上面。 通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。 可以通过给 git rebase 增加 -i 选项来交互式地运行变基。 必须指定想要重写多久远的历史,这可以通过告诉命令将要变基到的提交来做到。 + +例如,如果想要修改最近三次提交信息,或者那组提交中的任意一个提交信息, 将想要修改的最近一次提交的父提交作为参数传递给 git rebase -i 命令,即 HEAD~2^ 或 HEAD~3。 记住 ~3 可能比较容易,因为你正尝试修改最后三次提交;但是注意实际上指定了以前的四次提交,即想要修改提交的父提交: + +$ git rebase -i HEAD~3 +再次记住这是一个变基命令——在 HEAD~3..HEAD 范围内的每一个修改了提交信息的提交及其 所有后裔 都会被重写。 +不要涉及任何已经推送到中央服务器的提交——这样做会产生一次变更的两个版本,因而使他人困惑。 +输入 + +$ git commit --amend +修改提交信息,然后退出编辑器。 然后,运行 + +$ git rebase --continue +这个命令将会自动地应用另外两个提交,然后就完成了。 如果需要将不止一处的 pick 改为 edit,需要在每一个修改为 edit 的提交上重复这些步骤。 每一次,Git 将会停止,让你修正提交,然后继续直到完成。 + +### 查看远程仓库克隆地址 + +`git remote -v` + +关于`git`的工作区、缓存区可以看下图`index`标记部分的区域就是暂存区 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_stage.jpg?raw=true) + +从这个图中能看到缓存区的存在,这就是为什么我们新加或者修改之后都要调用`git add`方法后再调用`git commit`。 + +### stash(贮藏) + +stash会处理工作目录的脏的文件--即跟踪文件的修改与暂存的改动--然后将未完成的修改保存到一个栈上,而你可以在任何时候重新应用这些改动(甚至在不同的分支上)。 +假设你现在在`a`分支上开发新版本内容,已经开发了一部分,但是还没有达到可以提交的程度。你需要切换到`b`分支进行另一个升级的开发。那么可以把当前工作的改变隐藏起来,要将一个新的存根推到堆栈上,运行`git stash`命令。 + +```shell +$ git stash +Saved working directory and index state WIP on master: ef07ab5 synchronized with the remote repository +HEAD is now at ef07ab5 synchronized with the remote repository +``` +现在,工作目录是干净的,所有更改都保存在堆栈中。 现在使用`git status`命令来查看当前工作区状态: + +```shell +$ git status +On branch master +Your branch is up-to-date with 'origin/master'. + +nothing to commit, working directory clean +``` +现在,可以安全地切换分支并在其他地方工作。通过使用`git stash list`命令来查看已存在更改的列表。 + +```shell +$ git stash list +stash@{0}: WIP on master: ef07ab5 synchronized with the remote repository +``` +这个命令所储藏的修改可以使用`git stash list`列出,使用`git stash show`进行检查,并使用`git stash apply` +或`git stash apply stash@{2}`恢复(可能在不同的提交之上)。或者可以用git stash pop将最近的一次stash恢复。调用没有任何参数 +的`git stash`相当于`git stash save`。在17年10月下旬Git讨论废弃了git stash save命令,代之以现有的git stash push命令。 +可以使用git stash drop加上要移除的贮藏的名字来移除它。 + +### 区间提交 + +你想要查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。 你可以使用 master..experiment 来让 Git 显示这些提交。也就是“在 experiment 分支中而不在 master 分支中的提交”。 + +```shell +git log master..experiment +``` +#### 三点 + +这个语法可以选择出被两个引用之一包含但又不被两者同时包含的提交。 再看看之前双点例子中的提交历史。 如果你想看 master 或者 experiment 中包含的但不是两者共有的提交,你可以执行: + +```shell +git log master...experiment +``` +### Rebase操作 + +官网中将rebase翻译为变基,我感觉理解成改变基点,重新实现更容易理解一些。 +假设目前除master分支之外还有一个experiment分支: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-1.png?raw=true) +我们现在想要把master分支merge一下experiment分支的最新代码。整合分支最容易的方法是merge命令。它会把这两个分支的最新快照(C3和C4)以及两者 +最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(C5)并提交。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-2.png?raw=true) +其实,还有一种方法:你可以提取在C4中引入的补丁和修改,然后在C3的基础上应用一次。在Git中,这种操作就叫做变基(rebase)。 +你可以使用rebase命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。 +在这个例子中,你可以检出experiment分支,然后将它变基到master分支上: + +```shell +git checkout experiment +git rebase master +``` +它的原理是首先找到这两个分支(即当前分支experiment、变基操作的目标基底分支master)的最近共同祖先C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底C3,最后以此将之前另存为临时文件的修改依序引用。 +![将C4中的修改变基到C3上](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-3.png?raw=true) +现在回到master分支,进行一次快进合并。 + +```shell +git checkout master +git merge experiment +``` +![master分支的快进合并](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-4.png?raw=true) +此时C4'指向的快照就和上面直接用merge中的C5指向的快照一模一样了。这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 +你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。 +一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁--例如向某个他人维护的项目贡献代码时。在这种情况下,你首先在自己 +的分支里进行开发,当开发完成时你需要先将你的代码变基到orgin/master上,然后再向主项目提交修改。这样的话,该项目的维护者就不再需要 +进行整合工作,只需要快进合并即可。 +请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有 +次序依次应用到另一分支上,而合并是把最终结果合在一起。 + +##### 更有趣的变基例子 + +在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。假如你创建了一个主题分支server, +为服务端添加了一些功能,提交了C3和C4.然后从C3上创建了主题分支client,为客户端添加了一些功能,提交了C8和C9.最后,你回到server分支, +又提交了C10. +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-1.png?raw=true) +假设你希望将client中的修改合并到主分支并发布,但暂时并不想合并server中的修改,因为他们还需要经过更全面的测试。这时,你就可以使用 +git rebase命令的--onto选项,选中在client分支里但不在server分支里的修改(即C8和C9), +将它们在master分支上重放: `git rebase --onto master server client` +以上命令的意思是:取出client分支,找出它从server分支分歧之后的补丁,然后把这些补丁在master分支上重放一遍,让client看起来像直接 +基于master修改一样。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-2.png?raw=true) +现在可以快速合并master分支了(快速合并master分支,使之包含来自client分支的修改): + +```shell +git checkout master +git merge client +``` +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-3.png?raw=true) +接下来你决定将server分支中的修改也整合进来,使用git rebase 命令可以直接将主题分支(即这里的server)变基到基分支(即这里的master)上。这样做能省去你先切换到server分支,再对其进行变基命令的多个步骤。 +`git rebase master server` +如下图,将server中的修改变基到master上所示,server中的代码被“续”到了master后面。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-4.png?raw=true) +然后就可以快进合并主分支master了: + +```shell +git checkout master +git merge server +``` +至此,client和server分支中的修改都已经整合到主分支里了,你可以删除这两个分支,最终提交历史会变成下图的样子: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-5.png?raw=true) + +##### 变基的风险 + +奇妙的变基也并非完美无缺,要用它得遵守一条准则: +如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。 +变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。如果你已经将提交推送至某个仓库,而其他人也已经从该 +仓库拉取提交并进行了后续工作,此时,如果你用git rebase命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的 +提交进行整合,如果接下来你还要拉去并整合他们修改过的提交,事情就会变的一团糟。 +让我们来看一个在公开仓库上执行变基操作所带来的问题。假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。你的提交历史如下图: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-1.png?raw=true) +然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交 +记录就会变成这样: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-2.png?raw=true) +接下来,这个人又决定把合并操作回滚,改用变基。继而又用git push --force命令覆盖了服务器上的提交历史。之后你从服务器抓取更新,会发现 +多出来一些新的提交: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-3.png?raw=true) +结果就是你们两个人的处境都十分尴尬。如果你执行git pull命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库也会变成: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-4.png?raw=true) +这相当于是你将相同的内容又合并了一次,生成了一个新的提交。此时如果你执行git log命令,你会发现有两个调的作者、日期、日志居然是一样的, +这会令人感到混乱。此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。 +很明显对方并不想在提交历史中看到C4和C6,因为之前就是他把这两个提交丢弃的。 + +##### 用变基解决变基 + +如果你真的遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。 + +实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和——即 “patch-id”。 + +如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。 + +举个例子,如果遇到前面提到的 有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交 那种情境,如果我们不是执行合并,而是执行 git rebase teamone/master, Git 将会: + +- 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7) +- 检查其中哪些提交不是合并操作的结果(C2,C3,C4) +- 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4') +- 把查到的这些提交应用在 teamone/master 上面 + +从而我们将得到与 你将相同的内容又合并了一次,生成了一个新的提交 中不同的结果,如图 在一个被变基然后强制推送的分支上再次执行变基 所示。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-5.png?raw=true) +要想上述方案有效,还需要对方在变基时确保 C4' 和 C4 是几乎一样的。 否则变基操作将无法识别,并新建另一个类似 C4 的补丁(而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。 + +在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。 又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase teamone/master。 + +如果你只对不会离开你电脑的提交执行变基,那就不会有事。 如果你对已经推送过的提交执行变基,但别人没有基于它的提交,那么也不会有事。 如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交,那你就有大麻烦了,你的同事也会因此鄙视你。 + +如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。 + +#### 变基 vs. 合并 + +至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。 在回答这个问题之前,让我们退后一步,想讨论一下提交历史 +到底意味着什么。 + +有一种观点认为,仓库的提交历史即是记录实际发生过什么。它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是 +一种亵渎,你使用谎言掩盖了实际发生过的事情。如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来, +让后人能够查阅。 + +另一种观点则正好相反,他们认为提交历史是项目过程中发生的事。没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 +持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。 + +现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对 +提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。 + +总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。 + +### git fetch与git pull的区别 + +`git`中`fetch`命令是将远程分支的最新内容拉到了本地,但是`fecth`后是看不到变化的,如果查看当前的分支,会发现此时本地多了 +一个`FETCH_HEAD`的指针,`checkout`到该指针后才可以查看远程分支的最新内容。 + +而`git pull`的作用相当于`fetch`和`merge`的组合,会自动合并: + +```git +git fetch origin master +git merge FETCH_HEAD +``` +### git pull 与git pull --rebase的使用 + +使用下面的关系区别这两个操作: + +```git +git pull = git fetch + git merge +git pull --rebase = git fetch + git rebase +``` +`git rebase`的过程中,有时会有`conflit`这时`Git`会停止`rebase`并让用户去解决冲突,解决完冲突后, +用`git add`命令去更新这些内容,然后不用执行`git commit`,直接执行`git rebase --continue`这样`git`会继续`apply`余下的补丁。 + +## 参考 + +[Git官方文档](https://git-scm.com/book/zh/v2) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" new file mode 100644 index 00000000..02f9c51c --- /dev/null +++ "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -0,0 +1,848 @@ +HashMap实现原理分析 +=== +HashMap是Map接口的实现,元素以键值对的方式存储,并且允许使用null建和null值,因为key不允许重复,因此只能有一个键为null。 +HashMap被认为是Hashtable的增强版,HashMap是一个非线程安全的容器,如果想构造线程安全的Map考虑使用ConcurrentHashMap。 +HashMap是无序的,因为HashMap无法保证内部存储的键值对的有序性。 + +### 重要属性 + +- 初始容量 + HashMap的默认初始容量是由DEFAULT_INITIAL_CAPACITY属性管理的。 + `static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + HashMap的默认初始容量是 1 << 4 = 16, << 是一个左移操作,它相当于是: + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_1.webp) + +- 最大容量 + HashMap的最大容量是 + `static final int MAXIMUM_CAPACITY = 1 << 30;` + 这里是不是有个疑问?int 占用四个字节,按说最大容量应该是左移 31 位,为什么 HashMap 最大容量是左移 30 位呢?因为在数值计算中,最高位也就是最左位的位 是代表着符号为,0 -> 正数,1 -> 负数,容量不可能是负数,所以 HashMap 最高位只能移位到 2 ^ 30 次幂。 +- 默认负载因子 + HashMap的默认负载因子是 + `static final float DEFAULT_LOAD_FACTOR = 0.75f;` + float 类型所以用 .f 为单位,负载因子是和扩容机制有关,这里大致提一下,后面会细说。扩容机制的原则是当 HashMap 中存储的数量 > HashMap 容量 * 负载因子时,就会把 HashMap 的容量扩大为原来的二倍。 + HashMap 的第一次扩容就在 DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12 时进行。 +- 树化阈值 + HashMap 的树化阈值是 + `static final int TREEIFY_THRESHOLD = 8;` + + 在进行添加元素时,当一个桶中存储元素的数量 > 8 时,会自动转换为红黑树(JDK1.8 特性)。 +- 链表阈值 + HashMap 的链表阈值是 + `static final int UNTREEIFY_THRESHOLD = 6;` 在进行删除元素时,如果一个桶中存储元素数量 < 6 后,会自动转换为链表 - 扩容临界值 `static final int MIN_TREEIFY_CAPACITY = 64;` + 这个值表示的是当桶数组容量小于该值时,优先进行扩容,而不是树化 + + +## 原理 + +其底层数据结构是数组称之为哈希桶,每个桶(bucket)里面放的是链表,链表中的每个节点,就是哈希表中的每个元素。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_hash.webp) +哈希表中哈希函数的设计是相当重要的,这也是建哈希表过程中的关键问题之一。 +建立一个哈希表之前需要解决两个主要问题: + +- 构造一个合适的哈希函数,均匀性 H(key)的值均匀分布在哈希表中 +- 冲突的处理 + +冲突:在哈希表中,不同的关键字值对应到同一个存储位置的现象。 + +当一个值中要存储到HashMap中的时候会根据Key的值来计算出他的hash,通过hash值来确认存放到数组中的位置,如果发生hash冲突就以链表的形式存储,当链表过长的话,HashMap会把这个链表转换成红黑树来存储通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K / V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量 (超过Load Factor则resize为原来的2倍)。 +获取对象时,我们将K传给get()方法,它调用hashCode()计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。 + +扩容前后,哈希桶的长度一定会是2的次方。这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。 + +而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。 + +因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 + +但就算原本的hashCode()取的很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。 + +因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。即碰撞率会增大。 + +扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。 + +扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 + +high位= low位+原哈希桶容量,如果追加节点后,链表数量>=8,且只有数组长度大于64才处理,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时, +顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。 + +数组的特点:查询效率高,插入删除效率低。 +链表的特点:查询效率低,插入删除效率高。 + +在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。 + + +## JDK1.7 + +HashMap在JDK1.8中发生了改变,下面的部分是基于JDK1.7的分析。HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算, +哈希运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的。 +在HashMap里有这样的一句属性声明: + +```java +transient Entry[] table; +``` +可以看到Map是通过数组的方式来储存Entry,那Entry是神马呢?就是HashMap存储数据所用的类,它拥有的属性如下: +```java +static class Entry implements Map.Entry { + final K key; + V value; + Entry next; + final int hash; + ...//More code goes here +} +``` +看到next了吗?next就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry, +那么好吧,将新加的元素也放到第10个位置,将第10个位置的原有Entry赋值给当前新加的Entry的next属性。 +数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。 + +好了,总结一下: + +- HashMap中有一个叫table的Entry数组。这个数组存储了Entry类的对象。Entry类包含了key-value作为实例变量。 +- table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。 +- 每当往Hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。 根据key的hashcode()方法计算出来的hash值来决定在Entry数组的索引(所在的桶)。 +- 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。 +- key的equals()方法用来确保key的唯一性。 + + +接下来看一下put方法: + +```java +/** +* Associates the specified value with the specified key in this map. If the +* map previously contained a mapping for the key, the old value is +* replaced. +* +* @param key +* key with which the specified value is to be associated +* @param value +* value to be associated with the specified key +* @return the previous value associated with key, or null +* if there was no mapping for key. (A null return +* can also indicate that the map previously associated +* null with key.) +*/ +public V put(K key, V value) { + // 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。 + if (key == null) + return putForNullKey(value); + // 计算key的hash值,hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数 + int hash = hash(key.hashCode()); + // indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引 + int i = indexFor(hash, table.length); + + for (Entry e = table[i]; e != null; e = e.next) { + // 如果这个位置已经有了(也就是hash值一样)就用链表来存了。 开始迭代链表 + Object k; + // 直到Entry->next为null,就把当前的Entry对象变成链表的下一个节点。 + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { + // 如果我们再次放入同样的key会怎样呢?它会替换老的value。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)), + // 如果这个方法返回true,它就会用当前Entry的value来替换之前的value。 + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + // 如果计算出来的索引位置没有元素,就直接把Entry对象放到那个索引上。 + modCount++; + addEntry(hash, key, value, i); + return null; +} +``` +再看一下get方法: +```java +/** + * Returns the value to which the specified key is mapped, or {@code null} + * if this map contains no mapping for the key. + * + *

+ * More formally, if this map contains a mapping from a key {@code k} to a + * value {@code v} such that {@code (key==null ? k==null : + * key.equals(k))}, then this method returns {@code v}; otherwise it returns + * {@code null}. (There can be at most one such mapping.) + * + *

+ * A return value of {@code null} does not necessarily indicate that + * the map contains no mapping for the key; it's also possible that the map + * explicitly maps the key to {@code null}. The {@link #containsKey + * containsKey} operation may be used to distinguish these two cases. + * + * @see #put(Object, Object) + */ +public V get(Object key) { + // 如果key是null,table[0]这个位置的元素将被返回。 + if (key == null) + return getForNullKey(); + // 计算hash值 + int hash = hash(key.hashCode()); + // indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。 + for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { + Object k; + // 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。 + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) + return e.value; + } + return null; +} +``` + +## JDK1.8 + +在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变的,只是在一些地方做了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,且只有数组长度大于64才处理,将链表转换为红黑树。 + +利用红黑树快速增删改查的特点来提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。 + +HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算法极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_data_structure.jpg) + + +既然发生了变化,那这里就以1.8的源码基础上再做一下分析: + +1. 首先看一下对应的两个Node类: + + ```java + // 单链表 + static class Node implements Map.Entry { + // 用于定位数组的索引位置 + final int hash; + final K key; + V value; + // 链表的下一个node + Node next; + + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + // 每一个节点的hashcode值,是将key和value的hashCode值异或得到的 + public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + + public final boolean equals(Object o) { + if (o == this) + return true; + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + if (Objects.equals(key, e.getKey()) && + Objects.equals(value, e.getValue())) + return true; + } + return false; + } + } + ``` + + ```java + static class LinkedHashMapEntry extends HashMap.Node { + LinkedHashMapEntry before, after; + LinkedHashMapEntry(int hash, K key, V value, Node next) { + super(hash, key, value, next); + } + } + ``` + + ```java + // 红黑树 + static final class TreeNode extends LinkedHashMap.LinkedHashMapEntry { + TreeNode parent; // red-black tree links + TreeNode left; + TreeNode right; + TreeNode prev; // needed to unlink next upon deletion + boolean red; + TreeNode(int hash, K key, V val, Node next) { + super(hash, key, val, next); + } + /** + * Forms tree of the nodes linked from this node. + * @return root of tree + */ + final void treeify(Node[] tab) { + ... + } + + /** + * Returns a list of non-TreeNodes replacing those linked from + * this node. + */ + final Node untreeify(HashMap map) { + ... + } + + /** + * 红黑树的插入 + * Tree version of putVal. + */ + final TreeNode putTreeVal(HashMap map, Node[] tab, + int h, K k, V v) { + Class kc = null; + boolean searched = false; + TreeNode root = (parent != null) ? root() : this; + for (TreeNode p = root;;) { + int dir, ph; K pk; + if ((ph = p.hash) > h) + dir = -1; + else if (ph < h) + dir = 1; + else if ((pk = p.key) == k || (k != null && k.equals(pk))) + return p; + else if ((kc == null && + (kc = comparableClassFor(k)) == null) || + (dir = compareComparables(kc, k, pk)) == 0) { + if (!searched) { + TreeNode q, ch; + searched = true; + if (((ch = p.left) != null && + (q = ch.find(h, k, kc)) != null) || + ((ch = p.right) != null && + (q = ch.find(h, k, kc)) != null)) + return q; + } + dir = tieBreakOrder(k, pk); + } + + TreeNode xp = p; + if ((p = (dir <= 0) ? p.left : p.right) == null) { + Node xpn = xp.next; + TreeNode x = map.newTreeNode(h, k, v, xpn); + if (dir <= 0) + xp.left = x; + else + xp.right = x; + xp.next = x; + x.parent = x.prev = xp; + if (xpn != null) + ((TreeNode)xpn).prev = x; + moveRootToFront(tab, balanceInsertion(root, x)); + return null; + } + } + } + } + ``` + +2. 再来看一下HashMap的实现类及构造函数 + + ```java + public class HashMap extends AbstractMap + implements Map, Cloneable, Serializable { + // 默认的初始化容量大小,必须是2的倍数,默认是16,为啥必须是2的倍数,后面会说 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 + // 在构造函数中未指定时使用的负载因子。 + static final float DEFAULT_LOAD_FACTOR = 0.75f; + // 使用红黑树树而不链表的容器计数阈值 + static final int TREEIFY_THRESHOLD = 8; + // 用于在执行过程中取消使用红黑树(拆分)箱调整大小操作的箱计数阈值, + // 为啥这里转成红黑树是8,而从红黑树转成链表是6? 在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。 + static final int UNTREEIFY_THRESHOLD = 6; + // 哈希桶,存储数据的数组 + transient Node[] table; + // Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = (capacity * load factor),超过这个数量后就会进行重新resize(扩容),扩容一后的HashMap容量是之前容量的两倍 + int threshold; + + + public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + this.loadFactor = loadFactor; + this.threshold = tableSizeFor(initialCapacity); + } + + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs an empty HashMap with the default initial capacity + * (16) and the default load factor (0.75). + */ + public HashMap() { + this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted + } + } + ``` + +### 确定哈希桶数组索引位置 + +不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合, +所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的 +时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的 +离散性能。先看看源码的实现: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +hash函数是先拿到通过key的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。为什么要这样设计呢? +主要有两个原因 : + +1. 一定要尽可能降低hash碰撞,越分散越好。 +2. 算法一定要尽可能高效,因为这是高频操作,因此采用位运算。 + +因为hashcode是32位的int值int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散, +一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算, +得到的余数才能用来访问数组下标。源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。 + + +对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用所计算得到的Hash值总是相同的。我们首先想到的就是把hash值对数组长度 +取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的, +在HashMap中是这样做的:调用 (table.length -1) & hash来计算该对象应该保存在table数组的哪个索引处。 +这个方法非常巧妙,它通过 (table.length -1) & hash来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方, +当length总是2的n次方时, (table.length -1) & hash运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。 + +当length总是2的倍数时,h & (length-1) 将是一个非常巧妙的设计: + +- 假设h=5,length=16, 那么h & length - 1将得到 5; +- 如果h=6,length=16, 那么h & length - 1将得到 6 +- 如果h=15,length=16, 那么h & length - 1将得到 15; +- 但是当h=16时 , length=16 时,那么h & length - 1将得到0了; +- 当h=17时, length=16时,那么h & length - 1将得到1了。 + +这样保证计算得到的索引值总是位于table数组的索引之内。 + + +在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16), +主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中, +同时不会有太大的开销。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_hash.bmp) + + +### put + +```java + public V put(K key, V value) { + // 对key调用hash()方法获取hash值 + return putVal(hash(key), key, value, false, true); + } + + + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + // 1. 如果table还没初始化就初始化 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // 2. 根据键值key计算的hash值来得到插入的数组索引i,判断tab[i]是否为null,数组索引通过(n -1) & hash来获得 + if ((p = tab[i = (n - 1) & hash]) == null) + // 3. 如果tab[索引]的值为null,那就说明这个索引没有数组桶,直接新建一个数组桶 + tab[i] = newNode(hash, key, value, null); + else { + // 4. 否则就是目前已经存在该索引的数组桶了,要继续判断是链表还是红黑树。 + Node e; K k; + // 5. 判断table[索引]处的收个元素是否和key一样,如果首个就是那就直接覆盖value,不用再找了 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + // 6. 数组桶的首个元素就是,直接覆盖value的值 + e = p; + else if (p instanceof TreeNode) + // 7. 数组桶首个元素不是,并且链表是红黑树,红黑树直接插入键值对 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { + // 8. 目前为链表,开始遍历链表准备插入 + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + // 添加到目前的节点的next上 + p.next = newNode(hash, key, value, null); + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + // 判断链表长度是否大于8,如果大于直接转换成红黑树,插入键值对 + treeifyBin(tab, hash); + break; + } + + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + // 判断当前存在的键值对数量size是否超过了最大容量threshold,如果超过就调用resize扩容 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; + } +``` + +上面主要有两个方法一个是红黑树的插入,一个是treeifyBin()也就是把链表转换成红黑树 + +```java +// MIN_TREEIFY_CAPACITY 的值为64,若当前table的length不够,则resize() +// 将桶内所有的 链表节点 替换成 红黑树节点 +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + // 如果当前哈希表为空,或者哈希表中元素的个数小于树形化阈值(默认为 64),就去新建(扩容) + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); + // 如果哈希表中的元素个数超过了树形化阈值,则进行树形化 + // e 是哈希表中指定位置桶里的链表节点,从第一个开始 + else if ((e = tab[index = (n - 1) & hash]) != null) { + // 红黑树的头、尾节点 + TreeNode hd = null, tl = null; + do { + // 新建一个树形节点,内容和当前链表节点 e 一致 + TreeNode p = replacementTreeNode(e, null); + // 确定树头节点 + if (tl == null) + hd = p; + else { + p.prev = tl; + tl.next = p; + } + tl = p; + } while ((e = e.next) != null); + // 让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了 + if ((tab[index] = hd) != null) + hd.treeify(tab); + } +} +``` + +### resize()扩容 + +扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。 + +resize()方法用于初始化数组或数组扩容,每次扩容后容量为原来的2倍,并进行数据迁移。 + + +例如我们从16扩展为32时,具体的变化如下所示: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize1.bmp) + +因此元素在重新计算hash之后,因为n变为2倍,那n-1的mask范围在高位多1bit (红色),因此新的index就会发生这样的变化: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize2.bmp) + +因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了, +是0的话索引没变,是1的话索引变成 “原索引 + oldCap”。可以看看下图为16扩充为32的resize示意图: +![resize](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize3.bmp) + +这里有一个需要注意的点就是在JDK1.8 HashMap扩容阶段重新映射元素时不需要像1.7版本那样重新去一个个计算元素的hash值,而是通过hash & oldCap的值来判断,若为0则索引位置不变,不为0则新索引=原索引+旧数组长度,为什么呢?具体原因如下: + +因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap + +这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,**由于新增的1bit是0还是1可以认为是随机的,因此resize的过程, +均匀的把之前的冲突的节点分散到新的bucket了**。 + +```java +final Node[] resize() { + //oldTab 为当前表的哈希桶 + Node[] oldTab = table; + //当前哈希桶的容量 length + int oldCap = (oldTab == null) ? 0 : oldTab.length + //当前的阈值 + int oldThr = threshold; + //初始化新的容量和阈值为 0 + int newCap, newThr = 0; + //如果当前容量大于 0 + if (oldCap > 0) { + //超过最大值就不再扩充了,就只好随你碰撞去吧 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + //没超过最大值,就扩充为原来的 2 倍 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + //如果旧的容量大于等于默认初始容量 16, 那么新的阈值也等于旧的阈值的两倍 + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr;//那么新表的容量就等于旧的阈值 + else {// zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY;//此时新表的容量为默认的容量 16 + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值为默认容量 16 * 默认加载因子 0.75f = 12 + } + if (newThr == 0) {//如果新的阈值是 0,对应的是当前表是空的,但是有阈值的情况 + float ft = (float)newCap * loadFactor;//根据新表容量和加载因子求出新的阈值 + //进行越界修复 + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); + } + //更新阈值 + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + //根据新的容量构建新的哈希桶 + Node[] newTab = (Node[])new Node[newCap]; + //更新哈希桶引用 + table = newTab; + //如果以前的哈希桶中有元素 + //下面开始将当前哈希桶中的所有节点转移到新的哈希桶中 + if (oldTab != null) { + //把每个 bucket 都移动到新的 buckets 中 + for (int j = 0; j < oldCap; ++j) { + //取出当前的节点 e + Node e; + //如果当前桶中有元素,则将链表赋值给 e + if ((e = oldTab[j]) != null) { + //将原哈希桶置空以便 GC + oldTab[j] = null; + //如果当前链表中就一个元素,(没有发生哈希碰撞) + if (e.next == null) + //直接将这个元素放置在新的哈希桶里。 + //注意这里取下标是用哈希值与桶的长度-1。由于桶的长度是2的n次方,这么做其实是等于一个模运算。但是效率更高 + newTab[e.hash & (newCap - 1)] = e; + //如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + //如果发生过哈希碰撞,节点数小于 8 个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。 + else { // preserve order + //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即 low 位, 或者扩容后的下标,即 high 位。 high 位 = low 位 + 原哈希桶容量 + //低位链表的头结点、尾节点 + Node loHead = null, loTail = null; + //高位链表的头节点、尾节点 + Node hiHead = null, hiTail = null; + Node next;//临时节点 存放 e 的下一个节点 + do { + next = e.next; + //这里又是一个利用位运算 代替常规运算的高效点:利用哈希值与旧的容量,可以得到哈希值去模后,是大于等于 oldCap 还是小于 oldCap,等于 0 代表小于 oldCap,应该存放在低位,否则存放在高位 + if ((e.hash & oldCap) == 0) { + //给头尾节点指针赋值 + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + }//高位也是相同的逻辑 + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + }//循环直到链表结束 + } while ((e = next) != null); + //将低位链表存放在原 index 处, + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + //将高位链表存放在新 index 处 + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +``` + +再看一下往哈希表里插入一个节点的putVal函数,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。 +如果evict是false,那么表示是在初始化时调用的。 + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + //tab存放 当前的哈希桶, p用作临时链表节点 + Node[] tab; Node p; int n, i; + //如果当前哈希表是空的,代表是初始化 + if ((tab = table) == null || (n = tab.length) == 0) + //那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n + n = (tab = resize()).length; + //如果当前index的节点是空的,表示没有发生哈希碰撞。 直接构建一个新节点Node,挂载在index处即可。 + //这里再啰嗦一下,index 是利用 哈希值 & 哈希桶的长度-1,替代模运算 + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else {//否则 发生了哈希冲突。 + //e + Node e; K k; + //如果哈希值相等,key也相等,则是覆盖value操作 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p;//将当前节点引用赋值给e + else if (p instanceof TreeNode)//红黑树暂且不谈 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else {//不是覆盖操作,则插入一个普通链表节点 + //遍历链表 + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) {//遍历到尾部,追加新节点到尾部 + p.next = newNode(hash, key, value, null); + //如果追加节点后,链表数量》=8,则转化为红黑树 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + //如果找到了要覆盖的节点 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + //如果e不是null,说明有需要覆盖的节点, + if (e != null) { // existing mapping for key + //则覆盖节点值,并返回原oldValue + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + //这是一个空实现的函数,用作LinkedHashMap重写使用。 + afterNodeAccess(e); + return oldValue; + } + } + //如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。 + + //修改modCount + ++modCount; + //更新size,并判断是否需要扩容。 + if (++size > threshold) + resize(); + //这是一个空实现的函数,用作LinkedHashMap重写使用。 + afterNodeInsertion(evict); + return null; +} +``` + + + +扩容就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。 + +* 运算尽量都用位运算代替,更高效。 +* 对于扩容导致需要新建数组存放更多元素时,除了要将老数组中的元素迁移过来,也记得将老数组中的引用置null,以便GC +* 取下标 是用哈希值与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高 +* 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。 +* 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量 +* 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点 +* 如果追加节点后,链表数量 >= 8,则转化为红黑树 +* 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。 + + +### get + +```java +public V get(Object key) { + Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} + +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + // 找到索引,判断数组桶中的第一个元素是不是 + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + if ((e = first.next) != null) { + // 第一个元素不是的话,就看是不是红黑树还是链表,然后一直去找 + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + + +## JDK 7与JDK 8中关于HashMap的对比 + +1. JDK8为红黑树 + 链表 + 数组的形式,当桶内元素大于8时,且只有数组长度大于64才处理,便会树化。 +2. hash值的计算方式不同 (jdk 8 简化)。 +3. JDK7中table在创建hashmap时分配空间,而8中在put的时候分配。 +4. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;因为头插法会使链表发生反转,多线程环境下会产生环; +5. 在resize操作中,7 需要重新进行index的计算,而8不需要,通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原 index。 +6. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容; + +把链表转换成红黑树,树化需要满足以下两个条件: + +- 链表长度大于等于8 +- table数组长度大于等于64 +为什么table数组容量大于等于64才树化? + +因为当table数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。 + + +## 问题 + +1. 为什么capcity是2的幂? + + 因为算index时用的是(n-1) & hash,这样就能保证n-1是全为1的二进制数,如果不全为1的话,存在某一位为0,那么0,1与0与的结果都是0, + 这样便有可能将两个hash不同的值最终装入同一个桶中,造成冲突。所以必须是2的幂。这是为了服务key映射到index的Hash算法的, + 公式index=hashcode(key)&(length-1),初始长度(16-1),二进制为1111&hashcode结果为hashcode最后四位,能最大程度保持平均,二的幂数保证二进制为1,保持hashcode最后四位。这种算法在保持分布均匀之外,效率也非常高。 + +2. 为什么需要使用加载因子,为什么需要扩容呢? + + 因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多), + 扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。HashMap本来是以空间换时间,所以填充比没必要太大。 + 但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。 + +3. 为什么 HashMap 是线程不安全的,实际会如何体现? + + 如果多个线程同时使用put方法添加元素,假设正好存在两个put的key发生了碰撞 (hash值一样),那么根据HashMap的实现, + 这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。 + 如果多个线程同时检测到元素个数超过数组大小 * loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置 + 以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。 + 且会引起死循环的错误。 + +4. 与HashTable的区别 + + Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable, + 它的并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换, + 需要线程安全的场合可以用ConcurrentHashMap替换。 + + - 与之相比HashTable是线程安全的,且不允许key、value是null。 + - HashTable默认容量是11。 + - HashTable是直接使用key的hashCode(key.hashCode())作为hash值,不像HashMap内部使用static final int hash(Object key)扰动函数对key的hashCode进行扰动后作为hash值。 + - HashTable取哈希桶下标是直接用模运算%.(因为其默认容量也不是2的n次方。所以也无法用位运算替代模运算) + - 扩容时,新容量是原来的2倍+1。int newCapacity = (oldCapacity << 1) + 1; + - Hashtable是Dictionary的子类同时也实现了Map接口,HashMap是Map接口的一个实现类 + +5. 扩容的时候为什么1.8不用重新hash就可以直接定位原节点在新数据的位置呢? + + 这是由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,举个例子: + 扩容前长度为16,用于计算 (n-1) & hash 的二进制n - 1为0000 1111, + 扩容后为32后的二进制就高位多了1,============>为0001 1111。因为是&运算,1和任何数&都是它本身,那就分二种情况, + 原数据hashcode高位第4位为0和高位为1的情况;第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)。 + +6. HashMap和HashSet的区别: + +HashSet继承于AbstractSet接口,实现了Set、Cloneable、java.io.Serializable接口。HashSet不允许集合中出现重复的值。HashSet底层其实 +就是HashMap,所有对HashSet的操作其实就是对HashMap的操作。所以HashSet也不保证集合的顺序。 + + +参考: + +- [从 JDK7 与 JDK8 对比详细分析 HashMap 的原理与优化](https://allenmistake.top/2019/05/13/hashmap/) +- [面试必备:HashMap源码解析(JDK8)](https://blog.csdn.net/zxt0601/article/details/77413921) + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" "b/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" new file mode 100644 index 00000000..e24c20ec --- /dev/null +++ "b/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" @@ -0,0 +1,193 @@ +HTTP与HTTPS的区别 +=== + +## HTTP(超文本传输协议) + +超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。 + + + +- 1997年发布HTTP/1.1: 持久连接(长连接)、节约带宽、HOST域、管道机制、分块传输编码。 + + 长连接是指的TCP连接,也就是说复用的是TCP连接。即长连接情况下,多个HTTP请求可以复用同一个TCP连接,这就节省了很多TCP连接建立和断开的消耗。 + + 此外,长连接并不是永久连接的。如果一段时间内(具体的时间长短,是可以在header当中进行设置的,也就是所谓的超时时间),这个连接没有HTTP请求发出的话,那么这个长连接就会被断掉。 + +- 2015年发布HTTP/2:多路复用、服务器推送、头信息压缩、二进制协议等。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/http1.1vs2.jpg) + +多路复用:通过单一的HTTP/2连接请求发起多重的请求-响应消息,多个请求stream共享一个TCP连接,实现多留并行而不是依赖建立多个TCP连接。 + + + +缺点: + +- 通信使用明文(不加密),内容可能会被窃听 +- 不验证通信方的身份,因此有可能遭遇伪装 +- 无法证明报文的完整性,所以有可能已遭篡改 + +优点: + +- 传输速度快 + + + +### HTTP请求响应过程 + +你是不是很好奇,当你在浏览器中输入网址后,到底发生了什么事情?你想要的内容是如何展现出来的?让我们通过一个例子来探讨一下,我们假设访问的 URL 地址为 `http://www.someSchool.edu/someDepartment/home.index`,当我们输入网址并点击回车时,浏览器内部会进行如下操作 + +- DNS服务器会首先进行域名的映射,找到访问`www.someSchool.edu`所在的地址,然后HTTP客户端进程在80端口发起一个到服务器`www.someSchool.edu`的TCP连接(80端口是HTTP的默认端口)。在客户和服务器进程中都会有一个`套接字`与其相连。 +- HTTP客户端通过它的套接字向服务器发送一个HTTP请求报文。该报文中包含了路径`someDepartment/home.index`的资源,我们后面会详细讨论HTTP请求报文。 +- HTTP服务器通过它的套接字接受该报文,进行请求的解析工作,并从其`存储器(RAM 或磁盘)`中检索出对象 [www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到](http://www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到)HTTP响应报文中,并通过套接字向客户进行发送。 +- HTTP服务器随即通知TCP断开TCP连接,实际上是需要等到客户接受完响应报文后才会断开TCP连接。 +- HTTP客户端接受完响应报文后,TCP连接会关闭。HTTP客户端从响应中提取出报文中是一个HTML响应文件,并检查该HTML文件,然后循环检查报文中其他内部对象。 +- 检查完成后,HTTP客户端会把对应的资源通过显示器呈现给用户。 + +至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的`请求-响应`全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。 + + + + + +## HTTPS + +Https并非是应用层的一种新协议。只是http通信接口部分用SSL(安全套接字层)和TLS(安全传输层协议)代替而已。即添加了加密及认证机制的HTTP称为HTTPS(HTTP Secure). + +``` +HTTP + 加密 + 认证 + 完整性保护 = HTTPS +``` + + +如何保证安全 +--- + +一般来说网络安全关心三个问题:`CIA(confidentiality, integrity, availability)`. +那`https`在这三方面做的怎么样呢? +- `https`保证了`confidentiality`(你浏览的页面的内容如果被人中途看见,将会是一团乱码。不会发生比如和你用同一个无线网的人收到一个你发的数据包,打开来一看,就是你的密码啊银行卡信息啊) +- `intergrity`(你浏览的页面就是你想浏览的,不会被黑客在中途修改,网站收到的数据包也是你最初发的那个,不会把你的数据给换掉,搞一个大新闻) +- 最后一个`availability`几乎没有提供(虽然我个人认为会增加基础`DOS`等的难度,但是这个不值一提),不过`https`还提供了另一个`A`,`authentication`(你连接的是你连接的网站,而不是什么人在中途伪造了一个网站给你,专业上叫`Man In The Middle Attack`)。 +那`https`具体保护了啥?简单来说,保护了你从连接到这个网站开始,到你关闭这个页面为止,你和这个网站之间收发的所有信息,就连`url`的一部分都被保护了。同时`DNS querying`这一步也被保护了, +不会发生你输入`www.google.com`实际上跑到了另一个网站去了。 + + + +#### 使用两把密钥的公开密钥加密 + +公开密钥加密使用一对非对称的密钥。一把叫做私钥,另一把叫做公钥。私钥不能让其他任何人知道,而公钥则可以随意发布,任何人都可以获得。使用公钥加密方式,发送密文的一方使用对方的公钥进行加密处理,对方收到被加密的信息后,再使用自己的私钥进行解密。利用这种方式,不需要发送用来解密的私钥,也不必担心密钥被攻击者窃听而盗走。 + + +过程 +--- + +- 服务器把自己的公钥登录至数字证书认证机构。 +- 数字证书机构把自己的私有密钥向服务器的公开密码部署数字签名并颁发公钥证书。 +- 客户端拿到服务器的公钥证书后,使用数字证书认证机构的公开密钥,向数字证书认证机构验证公钥证书上的数字签名。以确认服务器公钥的真实性。 +- 使用服务器的公开密钥对报文加密后发送。 +- 服务器用私有密钥对报文解密。 + + +`HTTPS`通信的步骤 +--- + +- 客户端发送报文进行`SSL`通信。报文中包含客户端支持的`SSL`的指定版本、加密组件列表(加密算法及密钥长度等)。 +- 服务器应答,并在应答报文中包含`SSL`版本以及加密组件。服务器的加密组件内容是从接受到的客户端加密组件内筛选出来的。 +- 服务器发送报文,报文中包含公开密钥证书。 +- 服务器发送报文通知客户端,最初阶段`SSL`握手协商部分结束。 +- `SSL`第一次握手结束之后,客户端发送一个报文作为回应。报文中包含通信加密中使用的一种被称`Pre-master secret`的随机密码串。该密码串已经使用服务器的公钥加密。 +- 客户端发送报文,并提示服务器,此后的报文通信会采用`Pre-master secret`密钥加密。 +- 客户端发送`Finished`报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够完成成功,要以服务器是否能够正确解密该报文作为判定标准。 +- 服务器同样发送`Change Cipher Spec`报文。 +- 服务器同样发送`Finished`报文。 +- 服务器和客户端的`Finished`报文交换完毕之后,`SSL`连接就算建立完成。 +- 应用层协议通信,即发送`HTTP`响应。 +- 最后由客户端断开链接。断开链接时,发送`close_nofify`报文。 + + +`HTTPS`的工作原理 +--- + +`HTTPS`在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。`TLS/SSL`协议不仅仅是一套加密传输的协议,更是一件经过艺术家精心设计的艺术品,`TLS/SSL`中使用了非对称加密,对称加密以及`HASH`算法。握手过程的简单描述如下: +- 浏览器将自己支持的一套加密规则发送给网站。 +- 网站从中选出一组加密算法与`HASH`算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。 +- 获得网站证书之后浏览器要做以下工作: + + - 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。 + - 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。 + - 使用约定好的HASH计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。 + +- 网站接收浏览器发来的数据之后要做以下的操作: + + - 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。 + - 使用密码加密一段握手消息,发送给浏览器。 + +- 浏览器解密并计算握手消息的`HASH`,如果与服务端发来的`HASH`一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。 + +这里浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据,为后续真正数据的传输做一次测试。另外,`HTTPS`一般使用的加密与`HASH`算法如下: + +- 非对称加密算法:`RSA`,`DSA/DSS` +- 对称加密算法:`AES`,`RC4`,`3DES` +- `HASH`算法:`MD5`,`SHA1`,`SHA256` + + +其中非对称加密算法用于在握手过程中加密生成的密码,对称加密算法用于对真正传输的数据进行加密,而`HASH`算法用于验证数据的完整性。由于浏览器生成的密码是整个数据加密的关键,因此在传输的时候使用了非对称加密算法对其加密。非对称加密算法会生成公钥和私钥,公钥只能用于加密数据,因此可以随意传输,而网站的私钥用于对数据进行解密,所以网站都会非常小心的保管自己的私钥,防止泄漏。 + +`TLS`握手过程中如果有任何错误,都会使加密连接断开,从而阻止了隐私信息的传输。正是由于`HTTPS`非常的安全,攻击者无法从中找到下手的地方,于是更多的是采用了假证书的手法来欺骗客户端,从而获取明文的信息,但是这些手段都可以被识别出来, + + +优点 +--- + +尽管`HTTPS`并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但`HTTPS`仍是现行架构下最安全的解决方案,主要有以下几个好处: + +- 使用`HTTPS`协议可认证用户和服务器,确保数据发送到正确的客户机和服务器 +- `HTTPS`协议是由`SSL+HTTP`协议构建的可进行加密传输、身份认证的网络协议,要比`http`协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。 +- `HTTPS`是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。 +- 谷歌曾在2014年8月份调整搜索引擎算法,并称比起同等`HTTP`网站,采用`HTTPS`加密的网站在搜索结果中的排名将会更高。 + +缺点 +--- + +虽然说HTTPS有很大的优势,但其相对来说,还是存在不足之处的: + +- `HTTPS`协议握手阶段比较费时,会使页面的加载时间延长近`50%`,增加`10%`到`20%`的耗电; +- `HTTPS`连接缓存不如`HTTP`高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响; +- `SSL`证书需要钱,功能越强大的证书费用越高,个人网站、小网站没有必要一般不会用。 +- `SSL`证书通常需要绑定`IP`,不能在同一`IP`上绑定多个域名,`IPv4`资源不可能支撑这个消耗。 +- `HTTPS`协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。最关键的,`SSL`证书的信用链体系并不安全,特别是在某些国家可以控制`CA`根证书的情况下,中间人攻击一样可行。 + + + +### Https 请求慢的解决办法 + +1. 不通过DNS解析,直接访问IP + +2. 解决连接无法复用 + + http/1.0协议头里可以设置Connection:Keep-Alive或者Connection:Close,选择是否允许在一定时间内复用连接(时间可由服务器控制)。但是这对App端的请求成效不大,因为App端的请求比较分散且时间跨度相对较大。 + + 方案1.基于tcp的长连接 (主要) 移动端建立一条自己的长链接通道,通道的实现是基于tcp协议。基于tcp的socket编程技术难度相对复杂很多,而且需要自己定制协议。但信息的上报和推送变得更及时,请求量爆发的时间点还能减轻服务器压力(避免频繁创建和销毁连接) + + 方案2.http long-polling 客户端在初始状态发送一个polling请求到服务器,服务器并不会马上返回业务数据,而是等待有新的业务数据产生的时候再返回,所以链接会一直被保持。一但结束当前连接,马上又会发送一个新的polling请求,如此反复,保证一个连接被保持。 存在问题: 1)增加了服务器的压力 2)网络环境复杂场景下,需要考虑怎么重建健康的连接通道 3)polling的方式稳定性不好 4)polling的response可能被中间代理cache住 …… + + 方案3.http streaming 和long-polling不同的是,streaming方式通过再server response的头部增加“Transfer Encoding:chuncked”来告诉客户端后续还有新的数据到来 存在问题: 1)有些代理服务器会等待服务器的response结束之后才将结果推送给请求客户端。streaming不会结束response 2)业务数据无法按照请求分割 …… + + 方案4.web socket 和传统的tcp socket相似,基于tcp协议,提供双向的数据通道。它的优势是提供了message的概念,比基于字节流的tcp socket使用更简单。技术较新,不是所有浏览器都提供了支持。 + + + + + + + + + + + +---- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + \ No newline at end of file diff --git "a/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" "b/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" new file mode 100644 index 00000000..6e1a10a4 --- /dev/null +++ "b/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" @@ -0,0 +1,499 @@ +# JVM垃圾回收机制 + + +当前主流的商用程序语言(Java、C#)的内存管理子系统都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。 + +这个算法的基本思路就是通过一系列被称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所做过的路径称为"引用链"(Reference Chain), +如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。 + + +如下图: 对象object5、object6、object7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reachability_analysis.png) + + +在Java技术体系里面,固定可作为GC Roots的对象包括以下几种: + +- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。 + +- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 + +- 在方法区中常量引用的对象,譬如字符串常量池(String Table)中的引用。 + +- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 + +- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器。 + +- 所有被同步锁(synchronized关键字)持有的对象。 + +- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。 + + +除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。譬如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集)​,必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的)​,更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。 + + + + + +[Java内存模型](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.md)如下图所示,**堆和方法区是所有线程共有的**,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm.png) + +### 堆(Heap) + +Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。Java的自动内存管理主要是针对对象内存的回收和对象 +内存的分配。同时,Java自动内存管理最核心的功能是堆内存中对象的分配与回收。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法, +所以Java堆还可以细分为:新生代和老年代,再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收 +内存,或者更快地分配内存。 + +**堆空间的基本结构:** + +![](http://raw.githubusercontent.com/CharonChui/Pictures/master/java_heap.png) + +在垃圾回收的时候,我们往往将堆内存分成**新生代和老生代(大小比例1:2)**,新生代中由Eden和Survivor0,Survivor1组成,**三者的比例是8:1:1**, +新生代的回收机制采用**复制算法**,大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的 +年龄还会加1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄 +阈值,可以通过参数`-XX:MaxTenuringThreshold`来设置。老生代采用的回收算法是**标记整理算法。** + +#### 对象优先在eden区分配 + +目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 + +大多数情况下,对象在新生代中eden区分配。当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC. + +这里说一下Minor GC和Full GC有什么不同呢? + +- 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。 +- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。 + +#### 大对象直接进入老年代 + +大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。这样做主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 + +#### 长期存活的对象将进入老年代 + +既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对 +象一个对象年龄(Age)计数器。 + +如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在 +Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(一般都会说默认为 15 岁,其实默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6), +就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数`-XX:MaxTenuringThreshold`来设置。 + + +## 如何判断对象是垃圾 + +### 引用计数算法 + +在`JDK1.2`之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区, +堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象, +同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1, +而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候, +标志着这个对象已经没有引用了,可以回收了! +这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题,那就是互相引用的问题: + +```java +ObjA.obj = ObjB +ObjB.obj - ObjA +``` +这样的代码会产生如下引用情形`objA`指向`objB`,而`objB`又指向`objA`,这样当其他所有的引用都消失了之后, +`objA`和`objB`还有一个相互的引用,也就是说两个对象的引用计数器各为1, +而实际上这两个对象都已经没有额外的引用,已经是垃圾了。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/yinyongjishu.jpg) + + +### 根搜索算法 + +根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图, +从一个节点`GC ROOT`开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点, +当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/genshousuo.jpg) + +目前java中可作为GC Root的对象有: + +- 虚拟机栈中引用的对象(本地变量表) +- 方法区中静态属性引用的对象 +- 方法区中常量引用的对象 +- 本地方法栈中引用的对象(Native对象) + +垃圾回收算法 +--- + +而收集后的垃圾是通过什么算法来回收的呢: + +- 标记-清除算法 +- 复制算法 +- 标记整理算法 + +那我们就继续分析下这三种算法: + +### 标记-清除算法(Mark-Sweep) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_qingchu.png) + +原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象) + +适用场合: + +- 存活对象较多的情况下比较高效 +- 适用于年老代(即旧生代) + +缺点: + +- 标记清除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不连续的,这样给大对象分配内存的时候可能会提前触发full gc。 + +### 标记复制算法(mark-copy) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/fuzhisuanfa.png) + +原理:思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片 +问题,但是内存浪费很严重,相当于只能使用50%的内存。从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存 +(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉 + +适用场合: + +- 存活对象较少的情况下比较高效 +- 扫描了整个空间一次(标记存活对象并复制移动) +- 适用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少 + +缺点: + +- 需要一块儿空的内存空间 +- 需要复制移动对象 + +### 标记-整理算法(Mark-Compact) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_zhengli.png) + +原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)(可以发现前边这些就是标记-清除算法的原理), +清除完之后,将所有的存活对象左移到一起。避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理), +保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低GC的效率。 + +适用场合: + +- 用于年老代(即旧生代) + +缺点: + +- 需要移动对象,若对象非常多而且标记回收后的内存非常不完整,可能移动这个动作也会耗费一定时间 +- 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象) + +优点: + +- 不会产生内存碎片 + +### 分代收集算法(Generational Collection) + +上述三种算法,每种都有各自的优缺点,都不完美。在现代JVM中,往往是综合使用的,经过大量实际分析,发现内存中的对象,大致可以分为两类: +有些生命周期很短,比如一些局部变量/临时对象,而另一些则会存活很久,典型的比如websocket长连接中的connection对象,如下图: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/62226d097ac54148804119b4c239c802.png) + +纵向y轴可以理解分配内存的字节数,横向x轴理解为随着时间流逝(伴随着 GC),可以发现大部分对象其实相当短命,很少有对象能在GC后活下来。 +因此诞生了分代的思想,以Hotspot为例(JDK 7): + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/e4ff361d409b6939e6da49a06b6dc677.png) + +将内存分成了三大块:年青代(Young Genaration),老年代(Old Generation), 永久代(Permanent Generation),其中Young Genaration更是又细分为eden, +S0,S1三个区。 + +结合我们经常使用的一些jvm调优参数后,一些参数能影响的各区域内存大小值,示意图如下: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/d4e6583cd0ecfa60622526e7a4f13634.png) + +注:jdk8开始,用MetaSpace区取代了Perm区(永久代),所以相应的jvm参数变成-XX:MetaspaceSize及-XX:MaxMetaspaceSize。 + +以Hotspot为例,我们来分析下GC的主要过程: + +刚开始时,对象分配在eden区,s0(即:from)及s1(即:to)区,几乎是空着。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/8dc1423cc9f85658a946e204dc85ec18.png) + +随着应用的运行,越来越多的对象被分配到eden区。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/c9138916377192a8a2590aca3e888049.png) + +当eden区放不下时,就会发生minor GC(也被称为young GC),第1步当然是要先标识出不可达垃圾对象(即:下图中的黄色块),然后将可达对象, +移动到s0区(即:4个淡蓝色的方块挪到s0区),然后将黄色的垃圾块清理掉,这一轮过后,eden区就成空的了。 + +注:这里其实已经综合运用了“【标记 - 清理 eden】 + 【标记 - 复制 eden->s0】”算法。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/adc376cad0670c6993da8f32f3a88aba.png) + +随着时间推移,eden如果又满了,再次触发minor GC,同样还是先做标记,这时eden和s0区可能都有垃圾对象了(下图中的黄色块),注意:这时s1(即:to)区是空的, +s0区和eden区的存活对象,将直接搬到s1区。然后将eden和s0区的垃圾清理掉,这一轮minor GC后,eden和s0区就变成了空的了。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f21d18a0736d0976c4ecf14e82236cb2.png) + +继续,随着对象的不断分配,eden区可能又满了,这时会重复刚才的minor GC过程,不过要注意的是,这时候s0是空的,所以s0与s1的角色其实会互换, +即:存活的对象,会从eden和s1区,向s0区移动。然后再把eden和s1区中的垃圾清除,这一轮完成后,eden与s1区变成空的,如下图。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f765eb0f46635ae093516f35fb1eee66.png) + +对于那些比较“长寿”的对象一直在s0与s1中挪来挪去,一来很占地方,而且也会造成一定开销,降低gc效率,于是有了“代龄 (age)”及“晋升”。 + +对象在年青代的3个区 (edge,s0,s1) 之间,每次从1个区移到另1区,年龄+1,在young区达到一定的年龄阈值后,将晋升到老年代。下图中是8,即:挪动8次后, +如果还活着,下次minor GC时,将移动到Tenured区。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/2705a4ba41ed37bd535adab5b91ffb8f.png) + +下图是晋升的主要过程:对象先分配在年青代,经过多次Young GC后,如果对象还活着,晋升到老年代。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/6d24d96eb137f805c867736a750c2bd9.png) + +如果老年代,最终也放满了,就会发生major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记 - 清理 - 整理(压缩)的耗时通常会比较长, +会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少Full GC的原因。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/ebad989a70f78a40c561df9943234441.png) + +注:上面的过程主要来自oracle官网的资料,但是有一个细节官网没有提到,如果分配的新对象比较大,eden区放不下,但是old区可以放下时,会直接分配到 +old区(即没有晋升这一过程,直接到老年代了)。 + +下图引自阿里出品的《码出高效 -Java 开发手册》一书,梳理了 GC 的主要过程。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/e663bd3043c6b3465edc1e7313671d69.png) + + +## 垃圾回收器 + +不算最新出现的神器ZGC,历史上出现过7种经典的垃圾回收器。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/ab1c7be2fa4b5d180ffa1f43bf9dfce7.png) + +这些回收器都是基于分代的,把G1除外,按回收的分代划分,横线以上的3种:Serial ,ParNew, Parellel Scavenge都是回收年青代的, +横线以下的3种:CMS,Serial Old, Parallel Old都是回收老年代的。 + + + +垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。 +下面介绍一下`HotSpot(JDK 7)`虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。 + +### Serial/Serial Old +`Serial/Serial Old`收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时, +必须暂停所有用户线程。`Serial`收集器是针对新生代的收集器,采用的是`Copying`算法,`Serial Old`收集器是针对老年代的收集器, +采用的是`Mark-Compact`算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。 + +### ParNew + +ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程进行垃圾回收。 + +ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不 +能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数 +量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数 +来限制垃圾收集的线程数。 + +### Parallel Scavenge +Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的 +目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。 +停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务, +主要适合在后台运算而不需要太多交互的任务。 + +### CMS + +全称:Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是JDK 7中广泛使用的收集器,有必要多说一下,借一张网友的图说话: + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/e8/4e/e8b152cf510b06544c2a13bfb4fc564e.png) + +相对Serial Old收集器或Parallel Old 收集器而言,这个明显要复杂多了,从名字(Mark Swep)就可以看出,CMS收集器是基于标记清除算法实现的。 +它的收集过程分为四个步骤: + +1. 初始标记(initial mark) +2. 并发标记(concurrent mark) +3. 重新标记(remark) +4. 并发清除(concurrent sweep) + +分为4个阶段: + +- Inital Mark 初始标记 + 主要是标记GC Root开始的下级(注:仅下一级)对象,这个过程会STW,但是跟GC Root直接关联的下级对象不会很多,因此这个过程其实很快。 + +- Concurrent Mark 并发标记 + 根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有STW。 + +- Remark 再标志 + 为啥还要再标记一次?因为第2步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。 + 试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上, + 可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。 + 当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。 + +- Concurrent Sweep + 并行清理,这里使用多线程以“Mark Sweep- 标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。 + +等等,刚才我们不是提到过“标记清理”法,会留下很多内存碎片吗?确实,但是也没办法,如果换成“Mark Compact标记 - 整理”法,把垃圾清理后, +剩下的对象也顺便排整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了。 + +另外,由于这一步是并行处理,并不阻塞其它线程,所以还有一个副使用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮GC,才会被清理掉。 +不过由于CMS收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找 +到足够大的连续空间来分配当前对象,不得不提前开启一次Full GC。 + +为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行Full GC完 +开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参 +数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。 + +不幸的是,它作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms来收集老年代的时候, +新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也可以使用 +-XX:+UseParNewGC选项来强制指定它。 + +虽然仍不完美,但是从这4步的处理过程来看,以往收集器中最让人诟病的长时间STW,通过上述设计,被分解成二次短暂的STW,所以从总体效果上看,应用在GC期间 +卡顿的情况会大大改善,这也是CMS一度十分流行的重要原因。 + +### G1 + +G1的全称是Garbage-First,为什么叫这个名字,呆会儿会详细说明。鉴于CMS的一些不足之外,比如:老年代内存碎片化,STW时间虽然已经改善了很多, +但是仍然有提升空间。G1就横空出世了,它对于heap区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。G1收集器是一款面向服务端应用的垃圾收集器。 +HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点: + +1. 并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。 +2. 分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。 +3. 空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。 +4. 可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内, + 消耗在垃圾收集上的时间不得超过N毫秒。 + +在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生 +代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 + +虽然G1看起来有很多优点,实际上CMS还是主流。 + + +如下图,G1将heap内存区,划分为一个个大小相等(1-32M,2的n次方)、内存连续的Region区域,每个region都对应Eden、Survivor、Old、Humongous +四种角色之一,但是region与region之间不要求连续。 + +注:Humongous,简称H区是专用于存放超大对象的区域,通常>= 1/2 Region Size,且只有Full GC阶段,才会回收H区,避免了频繁扫描、复制 / 移动大对象。 + +所有的垃圾回收,都是基于1个个region的。JVM内部知道,哪些region的对象最少(即:该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快),这也是 Garbage-First 得名的由来,G 即是 Garbage 的缩写, 1 即 First。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f1a1bdecf4e56a4440707ce073d73f61.png) + +**G1 Young GC** + +young GC 前: + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/28b9077c545675b934d64ee643e30a9a.png) + +young GC 后: + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/be46c92240c7fd6dfcc6c11a48e4edd9.png) + +理论上讲,只要有一个 Empty Region(空区域),就可以进行垃圾回收。 + +![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/28ca063660f168b462ed8466faa77e68.png) + +由于 region 与 region 之间并不要求连续,而使用 G1 的场景通常是大内存,比如 64G 甚至更大,为了提高扫描根对象和标记的效率,G1 使用了二个新的辅助存储结构: + +Remembered Sets:简称 RSets,用于根据每个 region 里的对象,是从哪指向过来的(即:谁引用了我),每个 Region 都有独立的 RSets。(Other Region -> Self Region)。 + +Collection Sets :简称 CSets,记录了等待回收的 Region 集合,GC 时这些 Region 中的对象会被回收(copied or moved)。 + +RSets 的引入,在 YGC 时,将年青代 Region 的 RSets 做为根对象,可以避免扫描老年代的 region,能大大减轻 GC 的负担。注:在老年代收集 Mixed GC 时,RSets 记录了 Old->Old 的引用,也可以避免扫描所有 Old 区。 + + + +### ZGC (截止目前史上最好的 GC 收集器) + +在 G1 的基础上,做了很多改进(JDK 11 开始引入) + +#### 动态调整大小的 Region + +G1 中每个 Region 的大小是固定的,创建和销毁 Region,可以动态调整大小,内存使用更高效。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/4f/03/4fd12cf96cf020e56d071161fa56b603.png) + +#### 不分代,干掉了 RSets + +G1 中每个 Region 需要借助额外的 RSets 来记录“谁引用了我”,占用了额外的内存空间,每次对象移动时,RSets 也需要更新,会产生开销。 + +注:ZGC 没有为止,没有实现分代机制,每次都是并发的对所有 region 进行回收,不象 G1 是增量回收,所以用不着 RSets。不分代的带来的可能性能下降,会用下面马上提到的 Colored Pointer && Load Barrier 来优化。 + +#### 带颜色的指针 Colored Pointer + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/0e/5a/0e071b9c1124d0b7b09150d960c2925a.png) + +这里的指针类似 java 中的引用,意为对某块虚拟内存的引用。ZGC 采用了 64 位指针(注:目前只支持 linux 64 位系统),将 42-45 这 4 个 bit 位置赋予了不同的含义,即所谓的颜色标志位,也换为指针的 metadata。 + +finalizable 位:仅 finalizer(类比 c++ 中的析构函数)可访问; + +remap 位:指向对象当前(最新)的内存地址,参考下面提到的 relocation; + +marked0 && marked1 位:用于标志可达对象; + +这 4 个标志位,同一时刻只会有 1 个位置是 1。每当指针对应的内存数据发生变化,比如内存被移动,颜色会发生变化。 + +#### 读屏障 Load Barrier + +传统 GC 做标记时,为了防止其它线程在标记期间修改对象,通常会简单的 STW。而 ZGC 有了 Colored Pointer 后,引入了所谓的读屏障,当指针引用的内存正被移动时,指针上的颜色就会变化,ZGC 会先把指针更新成最新状态,然后再返回。(大家可以回想下 java 中的[ volatile 关键字](https://www.cnblogs.com/yjmyzz/p/6994796.html),有异曲同工之妙),这样仅读取该指针时可能会略有开销,而不用将整个 heap STW。 + +#### 重定位 relocation + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/0e/dd/0ee7dc65b940fe54d0b67a217174d2dd.png) + +如上图,在标记过程中,先从 Roots 对象找到了直接关联的下级对象 1,2,4。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/fd/e2/fd2797de4c1ba3b3f8e3b6b871d773e2.png) + +然后继续向下层标记,找到了 5,8 对象, 此时已经可以判定 3,6,7 为垃圾对象。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/95/28/9524b9346adfa6f78283b73b1b201728.png) + +如果按常规思路,一般会将 8 从最右侧的 Region 移动或复制到中间的 Region,然后再将中间 Region 的 3 干掉,最后再对中间 Region 做压缩 compact 整理。但 ZGC 做得更高明,它直接将 4,5 复制到了一个空的新 Region 就完事了,然后中间的 2 个 Region 直接废弃,或理解为“释放”,做为下次回收的“新”Region。这样的好处是避免了中间 Region 的 compact 整理过程。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/7b/22/7b18ada0e14c1fb65eabd73d7760a422.png) + +最后,指针重新调整为正确的指向(即:remap),而且上一阶段的 remap 与下一阶段的 mark 是混在一起处理的,相对更高效。 + +Remap 的流程图如下: + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/8f/c7/8f02e87ca3f90da08edb414e174fd8c7.png) + +#### 多重映射 Multi-Mapping + +这个优化,说实话没完全看懂,只能谈下自己的理解(如果有误,欢迎指正)。虚拟内存与实际物理内存,OS 会维护一个映射关系,才能正常使用。如下图: + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/4c/c4/4ce9d8741ffd7bcde179ed56ac56abc4.png) + +zgc 的 64 位颜色指针,在解除映射关系时,代价较高(需要屏蔽额外的 42-45 的颜色标志位)。考虑到这 4 个标志位,同 1 时刻,只会有 1 位置成 1(如下图),另外 finalizable 标志位,永远不希望被解除映射绑定(可不用考虑映射问题)。 + +所以剩下 3 种颜色的虚拟内存,可以都映射到同 1 段物理内存。即映射复用,或者更通俗点讲,本来 3 种不同颜色的指针,哪怕 0-41 位完全相同,也需要映射到 3 段不同的物理内存,现在只需要映射到同 1 段物理内存即可。 + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/ac/49/ac0d5ac0f5f32a7c1c23bde7f513b649.png) + +![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/44/2c/445295bc8132bf5aa33fd5aa6cc3c02c.png) + +#### 支持[ NUMA 架构](https://baike.baidu.com/item/NUMA/6906025?fr=aladdin) + +NUMA 是一种多核服务器的架构,简单来讲,一个多核服务器(比如 2core),每个 cpu 都有属于自己的存储器,会比访问另一个核的存储器会慢很多(类似于就近访问更快)。 + +相对之前的 GC 算法,ZGC 首次支持了 NUMA 架构,申请堆内存时,判断当前线程属是哪个 CPU 在执行,然后就近申请该 CPU 能使用的内存。 + +**小结**:革命性的 ZGC 经过上述一堆优化后,每次 GC 总体卡顿时间按官方说法 <10ms。注:启用 zgc,需要设置 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC。 + + + +# 与GC相关的常用参数 + +除了上面提及的一些参数,下面补充一些和GC相关的常用参数: + +- -Xmx: 设置堆内存的最大值。 +- -Xms: 设置堆内存的初始值。 +- -Xmn: 设置新生代的大小。 +- -Xss: 设置栈的大小。 +- -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。 +- -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。 +- -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden和survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、 + 吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和 + 停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。 +- -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。 +- -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。 +- -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于0的整数。收集器在工作时,会调整Java堆大小或者其他一些参数,尽可能地把 + 停顿时间控制在MaxGCPauseMills以内。 +- -XX:GCTimeRatio:设置吞吐量大小,它的值是一个0-100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。 + + + +参考: + +- [一文看懂 JVM 内存布局及 GC 原理](https://www.infoq.cn/article/3WyReTKqrHIvtw4frmr3) + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/JVM\346\236\266\346\236\204.md" "b/JavaKnowledge/JVM\346\236\266\346\236\204.md" new file mode 100644 index 00000000..34304137 --- /dev/null +++ "b/JavaKnowledge/JVM\346\236\266\346\236\204.md" @@ -0,0 +1,264 @@ +# JVM架构 + +Java的主要优势在于,它被设计为可在具有WORA(write once, run anywhere)概念的各种平台上运行-“一次编写,可在任何地方运行”。 + +Java源代码会被JDK内置的Java编译器(javac)编译成称为字节码(即.class文件)的中间状态, + +这些字节码是带有操作码操作数的十六进制格式,并且JVM可以将这些指令(无需进一步重新编译)解释为操作系统和底层硬件平台可以理解的本地语言。 +因此,字节码充当独立于平台的中间状态,该状态可在任何JVM之间移植,而与底层操作系统和硬件体系结构无关。 + +但是,由于JVM是为运行基础硬件和OS结构并与之通信而开发的,因此我们需要为我们的OS版本(Windows,Linux,Mac)和处理器体系结构(x86,x64) +选择适当的JVM版本。 + +我们大多数人都知道Java的上述故事,这里的问题是该过程中最重要的组成部分-JVM被当作一个黑匣子教给我们,它可以神奇地解释字节码并执行许多运行时活动, +例如JIT(程序执行期间进行实时)编译和GC(垃圾收集)。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/JVM_Architecture.png) + +## Class Loader子系统 + +JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统将class文件加载到内存中。这称为Java的动态类加载(dynamic class loading)功能。 +当它在运行时(而非编译时)首次引用类时,它将加载、链接和初始化类文件(.class)。 + +- 加载 + + Class Loader的主要任务就是把编译后的.class文件加载到内存中。通常,class加载过程会从加载main类(有声明static main()的类)。所有后续的 + 类加载尝试都是根据已运行的类中的类引用完成的,如以下情况所述: + + - 当字节码静态引用一个类时 + - 当字节码创建一个类对象时(例如: Person person = new Person("John")) + + 有3种类型的类加载器: + + - Bootstrap Class Loader + + 加载来自rt.jar的标准JDK类,例如引导路径中存在的核心Java API类-$ JAVA_HOME/jre/lib目录(例如java.lang。*包类)。它以C / C ++ + 之类的本地语言实现,并是Java中所有类加载器的父级。 + + - Extension Class Loader + + 将类加载请求委托给其父类Bootstrap,如果不成功,则从扩展路径中的扩展目录(例如,安全扩展功能)中加载-JAVA_HOME/jre/ext或java.ext.dirs + 系统指定的任何其他目录的类。该类加载器由Java中的sun.misc.Launcher$ExtClassLoader类实现。 + + - System/Application Class Loader + + 从系统类路径加载应用程序特定的类,可以使用-cp或-classpath命令来在程序执行时动态设置。它在内部使用映射到java.class.path的环境变量。 + 该类加载器由sun.misc.Launcher$AppClassLoader类用Java实现。 + + 除了上面讨论的3个主要的类加载器,程序员还可以直接在代码本身上创建用户定义的类加载器。这通过类加载器委托模型保证了应用程序的独立性。这种方法 + 用于Tomcat之类的Web应用程序服务器中,以使Web应用程序和企业解决方案独立运行。 + + 每个类加载器都有自己的名称空间来保存已加载的类。当类加载器加载类时,它将基于存储在名称空间中的完全合格的类名称(FQCN:Fully Qualified Class Name) + 搜索该类,以检查该类是否已被加载。即使该类具有相同的FQCN但具有不同的名称空间,也将其视为不同的类。不同的名称空间意味着该类已由另一个类加载器加载。 + + 它们遵循4个主要原则: + + - 可见性原则 + + 该原则指出子类加载器可以看到父类加载器加载的类,但是父类加载器找不到子类加载器加载的类。 + + - 唯一性原则 + + 该原则指出,父类加载的类不应再由子类加载器加载,并确保不会发生重复的类加载。 + + - 委托层次结构原则 + + 为了满足上述2个原则,JVM遵循一个委托层次结构来为每个请求装入的类选择类加载器。首先从最低的子级别开始,Application Class Loader将 + 接收到的类加载请求委托给Extension Class Loader,然后Extension Class Loader将该请求委托给Bootstrap Class Loader。如果在 + Bootstrap路径中找到了所请求的类,则将加载该类。否则,该请求将再次被转移回Extension Class Loader加载器中从该Loader的扩展路径或 + 自定义指定的路径中查找类。如果它也失败,则请求将返回到Application Class Loader中从该Loader的System类路径中查找该类,并且如果 + Application Class Loader也未能加载所请求的类,则将获得运行时异常— java.lang.ClassNotFoundException。 + + - 无法卸载原则 + + 即使类加载器可以加载类,但是无法卸载已加载的类。替代卸载功能的是可以删除当前的类加载器,并创建一个新的类加载器。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_class_loaders.png) + +- 链接 + + 在遵循以下属性的同时,链接涉及验证和准备已加载的类或接口,其直接父类和实现的接口以及其元素类型。 + + - 在链接一个类或接口之前,必须将其完全加载。 + - 在初始化类或接口之前,必须对其进行完全验证和准备(在下一步中)。 + - 如果在链接过程中发生错误,则会将其抛出到程序中的某个位置,在该位置,程序将采取某些操作,这些操作可能直接或间接地需要链接到错误所涉及的类或接口。 + + 链接分为以下三个阶段: + + - Verification + + 确保.class文件的正确性(代码是否根据Java语言规范正确编写?它是由有效的编译器根据JVM规范生成的吗?)。这是类加载过程中最复杂的测试过程, + 并且耗时最长。即使链接减慢了类加载过程的速度,它也避免了在执行字节码时多次执行这些检查的需要,从而使整体执行高效而有效。如果验证失败, + 则会引发运行时错误(java.lang.VerifyError)。例如,执行以下检查: + + ```java + - consistent and correctly formatted symbol table + - final methods / classes not overridden + - methods respect access control keywords + - methods have correct number and type of parameters + - bytecode doesn’t manipulate stack incorrectly + - variables are initialized before being read + - variables are a value of the correct type + ``` + - Preparation + + 为静态存储和JVM使用的任何数据结构(例如方法表)分配内存。静态字段已创建并初始化为其默认值,但是,在此阶段不执行任何初始化程序或代码, + 因为这是初始化的一部分。 + + - Resolution + + 用直接引用替换类型中的符号引用。通过搜索方法区域以找到引用的实体来完成此操作。 + +- 初始化 + + 在这里,将执行每个加载的类或接口的初始化逻辑(例如,调用类的构造函数)。由于JVM是多线程的,因此应在适当同步的情况下非常仔细地进行类或接口的 + 初始化,以避免其他线程尝试同时初始化同一类或接口(即使其成为线程安全的)。 + + 这是类加载的最后阶段,所有静态变量都分配有代码中定义的原始值,并且将执行静态块(如果有)。这是在类中从上到下,从类层次结构中的父级到子级逐行执行的。 + +### 对象的创建 + Java对象的创建过程: +1. 类加载检查 + 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析 + 和初始化过。如果没有,那必须先执行相应的类加载过程。 +2. 分配内存 + 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内 + 存从Java堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 +3. 初始化零值 + 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 +4. 设置对象头 + 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 + 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 +5. 执行init方法 + 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行, + 所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 + + +### 对象的内存布局 +在Hotspot虚拟机中,对象在内存中的布局可以分为3快区域:对象头、实例数据和对齐填充。 +Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象 +指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 +实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。 +对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍, +换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 + + +### 对象的访问定位 +建立对象就是为了使用对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种: +1. 句柄 + 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_refe_jubing.png) + +2. 直接指针 + 如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_refe_direct.png) + +这两种对象访问方式各有优势。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针, +而reference本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。 + +## Runtime Data Area + +运行时数据区是当JVM程序在OS上运行时分配的存储区。除了读取.class文件之外,Class Loader子系统还会生成相应的二进制数据,并将以下信息分别保存在每个类的Method区域中: + +- 加载的类及其直接父类的全限定名称 +- .class文件是否与Class / Interface / Enum相关 +- 修饰符,静态变量和方法信息等。 + +然后,对于每个已加载的.class文件,它都会按照java.lang包中的定义,恰好创建一个Class对象来表示堆内存中的文件。稍后,在我们的代码中,可以使用此 +Class对象读取类级别的信息(类名称,父名称,方法,变量信息,静态变量等)。 + +[具体可参考Java内存模型](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.md) + + + +## Execution Engine + +字节码的实际执行在这里进行。执行引擎通过读取分配给上述运行时数据区域的数据逐行执行字节码中的指令。 + +### Interpreter + +解释器解释字节码并一对一执行指令。因此,它可以快速解释一个字节码行,但是执行解释后的结果是一项较慢的任务。缺点是,当多次调用一个方法时,每次都需要新的解释和较慢的执行速度。 + +### Just-In-Time (JIT) Compiler + +如果只有解释器可用,则当多次调用一个方法时,每次也会进行解释,如果有效处理,这将是多余的操作。使用JIT编译器已经可以做到这一点。首先,它将整个字 +节码编译为本地代码(机器代码)。然后,对于重复的方法调用,它直接提供了本机代码,使用本机代码的执行比单步解释指令要快得多。本机代码存储在缓存中, +因此可以更快地执行编译后的代码。 + +但是,即使对于JIT编译器,编译所花费的时间也要比解释器所花费的时间更多。对于仅执行一次的代码段,最好对其进行解释而不是进行编译。同样,本机代码也 +存储在高速缓存中,这是一种昂贵的资源。在这种情况下,JIT编译器会在内部检查每个方法调用的频率,并仅在所选方法发生超过特定时间级别时才决定编译每个 +方法。自适应编译的想法已在Oracle Hotspot VM中使用。 + +当JVM供应商引入性能优化时,执行引擎有资格成为关键子系统。在这些工作中,以下4个组件可以大大提高其性能: + +- 中间代码生成器生成中间代码。 +- 代码优化器负责优化上面生成的中间代码。 +- 目标代码生成器负责生成本机代码(即机器代码)。 +- Profiler是一个特殊的组件,负责查找性能热点(例如,多次调用一种方法的实例) + +### 供应商的优化方法 + +- Oracle Hotspot VMs + + Oracle通过流行的JIT编译器模型Hotspot Compiler实现了其标准Java VM的2种实现。通过分析,它可以确定最需要JIT编译的热点,然后将代码的那些 + 性能关键部分编译为本机代码。随着时间的流逝,如果不再频繁调用这种已编译的方法,它将把该方法标识为不再是热点,并迅速从缓存中删除本机代码,并开 + 始在解释器模式下运行。这种方法可以提高性能,同时避免不必要地编译很少使用的代码。此外,Hotspot Compiler可以即时确定使用lining等技术来优化 + 已编译代码的最佳方式。编译器执行的运行时分析使它可以消除在确定哪些优化将产生最大性能收益方面的猜测。 + 这些虚拟机使用相同的运行时(解释器,内存,线程),但是将自定义构建JIT编译器的实现,如下所述。 + + Oracle Java Hotspot Client VM是Oracle JDK和JRE的默认VM技术。它通过减少应用程序启动时间和内存占用量而在客户端环境中运行应用程序时进行了优化,以实现最佳性能。 + Oracle Java Hotspot Server VM旨在为在服务器环境中运行的应用程序提供最高的程序执行速度。此处使用的JIT编译器称为“高级动态优化编译器”, + 它使用更复杂和多样化的性能优化技术。通过使用服务器命令行选项(例如Java服务器MyApp)来调用Java HotSpot Server VM。 + + Oracle的Java Hotspot技术以其快速的内存分配,快速高效的GC以及易于在大型共享内存多处理器服务器中扩展的线程处理能力而闻名。 + +- IBM AOT (Ahead-Of-Time) Compiling + + 这里的特长是这些JVM共享通过共享缓存编译的本机代码,因此已经通过AOT编译器编译的代码可以由另一个JVM使用,而无需编译。另外,IBM JVM通过使用 + AOT编译器将代码预编译为JXE(Java可执行文件)文件格式,提供了一种快速的执行方式。 + + + +### Garbage Collector (GC) + +只要引用了一个对象,JVM就会认为它是活动的。一旦不再引用对象,因此应用程序代码无法访问该对象,则垃圾收集器将其删除并回收未使用的内存。通常,垃圾 +回收是在后台进行的,但是我们可以通过调用System.gc()方法来触发垃圾回收(同样,无法保证执行。因此,请调用Thread.sleep(1000)并等待GC完成)。 +[具体内存回收部分请看JVM垃圾回收机制](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.md) + + + +### JVM线程 + +我们讨论了如何执行Java程序,但没有具体提及执行程序。实际上,为了执行我们前面讨论的每个任务,JVM并发运行多个线程。这些线程中的一些带有编程逻辑, +并且是由程序(应用程序线程)创建的,而其余的则是由JVM本身创建的,以承担系统中的后台任务(系统线程)。 + +主应用程序线程是作为调用公共静态void main(String [])的一部分而创建的主线程,而所有其他应用程序线程都是由该主线程创建的。 +应用程序线程执行诸如执行以main()方法开头的指令,在Heap区域中创建对象(如果它在任何方法逻辑中找到新关键字)之类的任务等。 + +主要的系统线程有: + +- Compiler threads + + 在运行时,这些线程将字节码编译为本地代码。 + +- GC threads + + 所有与GC相关的活动均由这些线程执行。 + +- Periodic task thread + + 计划周期性操作执行的计时器事件(即中断)由该线程执行。 + +- Signal dispatcher thread + + 该线程接收发送到JVM进程的信号,并通过调用适当的JVM方法在JVM内部对其进行处理。 + +- VM thread + + 前提条件是,某些操作需要JVM到达安全点才能执行,在该点不再进行对Heap区域的修改。这种情况的示例是“世界停止”垃圾回收,线程堆栈转储,线程挂起 + 和有偏向的锁吊销。这些操作可以在称为VM线程的特殊线程上执行。 + + + diff --git "a/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" new file mode 100644 index 00000000..8e75224f --- /dev/null +++ "b/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -0,0 +1,239 @@ +Java内存模型 +=== + +Java虚拟机(Java Virtual Machine)在执行Java程序的过程中,会把它管理的内存划分为五个不同的数据区域(Heap Memory、 +Stack Memory、Method Area、PC、Native Stack Memory),这些区域都有各自的用途、创建时间、销毁时间: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_memory_model.jpeg) + +## Program Counter Register + +程序计数器是一块较小的内存空间,严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹 +不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照 +被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储, +我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。可以看作是当前线程所执行的字节码的行号指示器。 + +从上面的介绍中我们知道程序计数器主要有两个作用: + +- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 +- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 + +注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。 + + + +## Heap Memory + +所有的对象实例和数组都存放到Heap内存中。Heap内存也称为共享内存。多线程可以共享这里面的数据。 + +- 堆内存在JVM启动的时候被加载(初始大小: -Xms) +- 堆内存在程序运行时会增加或减少 +- 最小值: -Xmx +- 从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden空间、From Survivor空间(s0)、To Survivor空间(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_heap_memory.png) + +在并发编程中,多个线程之间采取什么机制进行通信(信息交换),什么机制进行数据的同步?在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的。 + +## Stack Memory + +栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的线程栈,线程stack中包含有关线程调用了哪些方法以达到当前执行点的信息,也可以称之为调用栈,只要线程执行代码,调用栈就会发生改变。在这个栈中又会包含多个栈帧(Stack Frame),栈是由一个个栈帧组成,这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量表、操作栈和方法返回值等信息。 + +局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 + +每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。 + +线程stack中同样会包含该线程调用栈中所有方法执行所需要的本地变量,一个线程只能获取到它自己对应的线程stack。一个线程创建的本地变量对于其他任何线程都是不可见的。即使两个线程执行完全相同的代码,这两个线程仍然会在各自自己对应的线程stack中创建自己的本地变量。 +所有基础类型的局部变量(boolean,byte,short,char,int,long,float,double)都被保存在自己的线程stack中。一个线程可以将一个基础变量的副本传递给另一个线程,但是它不能共享原始局部变量本身。 + +堆内存包含在Java应用程序中创建的所有对象,而不管创建该对象的线程是什么。这包括基本类型的对象版本(例如Byte,Integer,Long等)。创建对象并将其分配给局部变量,或者将其创建为另一个对象的成员变量都没有关系,该对象仍存储在堆中。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java-memory-model-1.png) + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/stack_heap.png) + +- 局部变量可以是原始类型,在这种情况下,它完全保留在线程堆栈中。 +- 局部变量也可以是对对象的引用。在这种情况下,引用(局部变量)存储在线程堆栈中,但是对象本身(如果有)存储在堆中。 +- 一个对象可能包含方法,而这些方法可能包含局部变量。即使该方法所属的对象存储在堆中,这些局部变量也存储在线程堆栈中。 +- 对象的成员变量与对象本身一起存储在堆中。不管成员变量是原始类型时,以及它是对对象的引用时,都是如此。 +- 静态类变量也与类定义一起存储在堆中。 +- 引用对象的所有线程都可以访问堆上的对象。当线程可以访问对象时,它也可以访问该对象的成员变量。如果两个线程同时在同一个对象上调用一个方法,则它们都将有权访问该对象的成员变量,但是每个线程将拥有自己的局部变量副本。 + +Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError: + +- StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。 +- OutOfMemoryError: 若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。 + +## Method Area + +方法区也称”永久代“,它用于存储虚拟机加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的属性(Field)信息(属性信息包括属性名、属性类型、属性修饰符(public, private, protected,static,final volatile,transient的某个子集))、类中的方法信息(方法的相关信息包括:方法名, 方法的返回类型(或 void), 方法参数的数量和类型(有序的),方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)),以及即时编译器编译后的代码(每个方法:字节码,操作数堆栈大小,局部变量大小,局部变量表,异常表;异常表中的每个异常处理程序:起点,终点,处理程序代码的PC偏移,捕获到的异常类的常量池索引),当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。 + +方法区默认最小值为16MB,最大值为64MB(64位JVM由于指针膨胀,默认是85M),可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。它是一片连续的堆空间,永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集 + +方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。 + +方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。 + +正因为如此相像,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但目前实际上是与Java堆分开的(Non-Heap)。Non-Heap主要包括的内容如下: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_no_heap.webp) + +方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。 + +### Constant Pool + +常量池也称为运行时常量池(Runtime Constant Pool),用于存放编译期生成的各种字面量和符号引用。 + +常量池本身是方法区中的一个数据结构。既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。 + +常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。 + +JVM会加载、链接、初始化class文件。一个class文件会把其所有所有符号引用都保留在一个位置,即常量池中。 + +每个class文件都会有一个对应constant pool。但是class文件中的常量池显然是不够的,因为需要再JVM上执行。这种情况下,需要runtime constant pool来服务JVM的运行。 + +Java虚拟机加载的每个类或接口都有其常量池的内部版本,称为运行时常量池(runtime constant pool)。运行时常量池是特定于实现的数据结构,它与类文件中的常量池是一一对应映射的。因此,在最初加载类型之后,该类型的所有符号引用都驻留在该类型的运行时常量池中。包括字符串常量,类和接口名称,字段名称以及类中引用的其他常量。 + +#### String Constant Pool + +在Constant Pool中还有一个单独存放字符串的String Constant Pool。 + +```java +String str1 = "abcd"; +String str2 = new String("abcd"); +System.out.println(str1==str2);//false +``` +这两种不同的创建方法是有差别的,第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间创建一个新的对象。 +```java +String str1 = "str"; +String str2 = "ing"; + +String str3 = "str" + "ing";//常量池中的对象 +String str4 = str1 + str2; //在堆上创建的新的对象 +String str5 = "string";//常量池中的对象 +System.out.println(str3 == str4);//false +System.out.println(str3 == str5);//true +System.out.println(str4 == str5);//false +``` +尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用StringBuilder或者StringBuffer。 + +Java 基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 +两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。 +```java +Integer i1 = 33; +Integer i2 = 33; +System.out.println(i1 == i2);// 输出true +Integer i11 = 333; +Integer i22 = 333; +System.out.println(i11 == i22);// 输出false +Double i3 = 1.2; +Double i4 = 1.2; +System.out.println(i3 == i4);// 输出false +``` + +### Method Area & Constant Pool改动 + +很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。 + +在JDK1.7中,将字符串常量池和静态变量从方法区域中移到堆中,其余的运行时常量池仍在方法区域中,即hotspot中的永久生成。 所有的被intern的String被存储在PermGen区.PermGen区使用-XX:MaxPermSize=N来设置最大大小,但是由于应用程序string.intern通常是不可预测和不可控的,因此不好设置这个大小。设置不好的话,常常会引起java.lang.OutOfMemoryError: PermGen space。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java7_method_constant_pool.jpg) + +在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。string constant pool仍然还是在Heap内存中。runtime constant pool也仍然在方法区。但是方法区的实现已经改成使用Metaspace。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java8_memory_method.jpg) + +为什么要移除永久代,改为元空间呢? + +> Metaspace与PermGen之间最大的区别在于:Metaspace并不在虚拟机中,而是使用本机内存。如果没有使用-XX:MaxMetaspaceSize来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。 +> 但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。 +> +> 如果类元数据的空间占用达到MaxMetaspaceSize设置的值,将会触发对象和类加载器的垃圾回收。java.lang.OutOfMemoryError: Metaspace space + +从用户角度来看,主要的区别是,默认情况下,Metaspace自动增加其大小(达到基础操作系统所提供的大小),而PermGen始终具有固定的最大大小。您可以使用JVM参数为Metaspace设置固定的最大值,但不能使PermGen自动增加。 + +在很大程度上,这只是名称的更改。早在引入PermGen时,就没有Java EE或动态类的加载(取消加载),因此,一旦加载了一个类,该类便一直停留在内存中,直到JVM关闭为止,从而实现了永久生成。如今,可以在JVM的生命周期内加载和卸载类,因此对于保留元数据的区域,Metaspace更有意义。 + +## Native Method Stack + +本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。如Java使用c或c++编写的接口服务时,代码在此区运行,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/RDA.png) +先看一张图,这张图能很清晰的说明JVM内存结构的布局和相应的控制参数: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_memory_archi_param.jpeg) + +## 主内存和工作内存: + +Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不同步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,因为后者是线程私有的,不会共享,当然不存在数据竞争问题(如果局部变量是一个reference引用类型,它引用的对象在Java堆中可被各个线程共享,但是reference引用本身在Java栈的局部变量表中,是线程私有的)。为了获得较高的执行效能,Java内存模型并没有限制执行引起使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。 + +JMM(Java Memory Model)规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。 + +由上述对JVM内存结构的描述中,我们知道了堆和方法区是线程共享的。而局部变量,方法定义参数和异常处理器参数就不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。 + +Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 + +假设线程A与线程B之间如要通信的话,必须要经历下面2个步骤: + +1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。 +2. 线程B到主内存中去读取线程A之前已更新过的共享变量。 + +## 重排 + +在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。 + +这里说的重排序可以发生在好几个地方:编译器、运行时、JIT等,比如编译器会觉得把一个变量的写操作放在最后会更有效率,编译后,这个指令就在最后了(前提是只要不改变程序的语义,编译器、执行器就可以这样自由的随意优化),一旦编译器对某个变量的写操作进行优化(放到最后),那么在执行之前,另一个线程将不会看到这个执行结果。 + +当然了,写入动作可能被移到后面,那也有可能被挪到了前面,这样的“优化”有什么影响呢?这种情况下,其它线程可能会在程序实现“发生”之前,看到这个写入动作(这里怎么理解,指令已经执行了,但是在代码层面还没执行到)。通过内存屏障的功能,我们可以禁止一些不必要、或者会带来负面影响的重排序优化,在内存模型的范围内,实现更高的性能,同时保证程序的正确性。 + +下面我们来看一个重排序的例子: +```java +Class Reordering { + int x = 0, y = 0; + public void writer() { + x = 1; + y = 2; + } + public void reader() { + int r1 = y; + int r2 = x; + } +} +``` +假设这段代码有2个线程并发执行,线程A执行writer方法,线程B执行reader方法,线程B看到y的值为2,因为把y设置成2发生在变量x的写入之后(代码层面),所以能断定线程B这时看到的x就是1吗? + +当然不行!因为在writer方法中,可能发生了重排序,y的写入动作可能发在x写入之前,这种情况下,线程B就有可能看到x的值还是0。 + +在Java内存模型中,描述了在多线程代码中,哪些行为是正确的、合法的,以及多线程之间如何进行通信,代码中变量的读写行为如何反应到内存、CPU缓存的底层细节。 + + +## 问题: 匿名内部类访问局部变量时,为什么这个局部变量必须用final修饰? +这个问题并不是很严谨,严格来说应该是Java 1.8之前,匿名内部类访问局部变量时,才需要用final修饰。 +我们平时经常会用匿名内部类访问局部变量的情况,编译器都会提示我们要对这个局部变量加final修饰,但是我们却并没有去仔细考虑过这是为什么? + +上面说到类和成员变量保存到堆内存中。而局部变量则保存在栈内存中。 +假设在main()方法中有一个局部变量a,然后main()方法里面又去创建了一个匿名内部类使用该局部变量a。 +a是在栈内存中的,当main()方法执行结束,a就被清理了。但是你创建的内部类中的方法却可能在main()方法执行完成后再去执行,但是这时候局部变量已经不存在了,那怎么解决这个问题呢? +因此实际上是在访问它的副本,而不是访问原始的局部变量。 + +在Java的参数传递中,当基本类型作为参数传递时,传递的是值的拷贝,无论你怎么改变这个拷贝,原值是不会改变的;当对象作为参数传递时,传递的是对象的引用的拷贝,无论你怎么改变这个新的引用的指向,原来的引用是不会改变的(当然如果你通过这个引用改变了对象的内容,那么改变实实在在发生了)。而当final修饰基本类型变量时,不可更改其值,当final修饰引用变量时,不可更改其指向,只能更改其对象的内容。 + +在Java中内部类会持有外部类的引用和方法中参数的引用,当反编译class文件后,内部类的class文件的构造函数参数中会传入外部类的对象以及方法内局部变量,不管是基本数据类型还是引用变量,如果重新赋值了,会导致内外指向的对象不一致,所以java就暴力的规定使用final,不能重新赋值。 +所以用final修饰实际上就是为了变量值(数据)的一致性。 这里所说的数据一致性,对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。 + + +当然在JDK 1.8及以后,看起来似乎编译器取消了这种限制,没有被声明为final的变量或参数也可以在匿名内部类内部被访问了。但实际上是因为Java 8引入了effectively final的概念(A variable or parameter whose value is never changed after it is initialized is effectively final)。对于effectively final(事实上的final),可以省略final关键字,本质不变,所以,实际上是诸如effectively final的变量或参数被Java默认为final类型,所以才不会报错,而上述的根本原因没有任何变化。 + + + It's about the scope of variables , Because anonymous inner classes appear inside a method , If it wants to access the parameters of the method or the variables defined in the method , Then these parameters and variables must be modified to final. Because although anonymous inner classes are inside methods , But when it's actually compiled , Inner classes are compiled into Outer.Inner, This means that the inner class is at the same level as the method in the outer class , A variable or parameter in a method in an external class is just a local variable of the method , The scope of these variables or parameters is only valid inside this method . Because internal classes and methods are at the same level when compiling , So the variables or parameters in the method are only final, Internal classes can be referenced . + +--- + +参考 +--- +- [Java (JVM) Memory Types](https://javapapers.com/core-java/java-jvm-memory-types/) +- [Java 8: From PermGen to Metaspace](https://dzone.com/articles/java-8-permgen-metaspace) +- [Interview question Series Part 5: JDK runtime constant pool, string constant pool, static constant pool, are you stupid and confused?](https://javamana.com/2020/11/20201113132526144q.html) +- [Where Has the Java PermGen Gone?](https://www.infoq.com/articles/Java-PERMGEN-Removed/) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" "b/JavaKnowledge/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" similarity index 88% rename from "Java\345\237\272\347\241\200/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" rename to "JavaKnowledge/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" index 4f5ca238..586f63be 100644 --- "a/Java\345\237\272\347\241\200/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" +++ "b/JavaKnowledge/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" @@ -1,1901 +1,1696 @@ -Java基础面试题 -=== - -本部分全部内容是根据张孝祥老师的Word文档整理而来。只不过是为了方便观看,把代码部分用`markdown`来展示。整理时脑海中不断回忆起张老师上课的情景,真是怀念。 - -1. 一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? - 可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。 -2. Java有没有goto? - java中的保留字,现在没有在java中使用。 -3. 说说&和&&的区别。 - &和&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。 - &&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式,例如,对于if(str != null && !str.equals(“”))表达式,当str为null时, - 后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。If(x==33 & ++y>0) y会增长, - If(x==33 && ++y>0)不会增长&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作, - 我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。 - 备注:这道题先说两者的共同点,再说出&&和&的特殊之处,并列举一些经典的例子来表明自己理解透彻深入、实际经验丰富。 -4. 在JAVA中如何跳出当前的多重嵌套循环? - 在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如, - ```java - ok: - for(int i=0;i<10;i++) - { - for(int j=0;j<10;j++) - { - System.out.println(“i=” + i + “,j=” + j); - if(j == 5) break ok; - } - } - ``` - 另外,我个人通常并不使用标号这种方式,而是让外层的循环条件表达式的结果可以受到里层循环体代码的控制,例如,要在二维数组中查找到某个数字。 - ```java - int arr[][] = {{1,2,3},{4,5,6,7},{9}}; - boolean found = false; - for(int i=0;i“zxx,male,28,30000”?Person, - 既然大家都要这么干,并且没有个统一的干法,于是,sun公司就提出一种统一的解决方案,它会把对象变成某个格式进行输入和输出, - 这种格式对程序员来说是透明(transparent)的,但是,我们的某个类要想能被sun的这种方案处理,必须实现Serializable接口。 - ObjectOutputStream.writeObject(obj); - Object obj = ObjectInputStream.readObject(); - 假设两年前我保存了某个类的一个对象,这两年来,我修改该类,删除了某个属性和增加了另外一个属性,两年后,我又去读取那个保存的对象,或有什么结果? - 未知!sun的jdk就会蒙了。为此,一个解决办法就是在类中增加版本后,每一次类的属性修改,都应该把版本号升级一下,这样,在读取时, - 比较存储对象时的版本号与当前类的版本号,如果不一致,则直接报版本号不同的错! -20. 面向对象的特征有哪些方面 - 面向对象是相对于面向过程而言的,面向过程强调的是功能,面向对象强调的是将功能封装进对象强调具备功能的对象, - - 思想特点好处: - - 是符合人们思考习惯的一种思想; - - 将复杂的事情简单化了; - - 将程序员从执行者变成了指挥者; - 注:比如我要达到某种结果我要是用某个功能,我就寻找能帮我达到该结果的功能的对象,如果该对象具备了该功能 - 那么我拿到该对象,也就可以使用该对象的功能。如我要洗衣服我就买洗衣机,至于怎么洗我不管。 - - 面向对象的编程语言有封装、继承 、多态等3个主要的特征。 - - 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式 - 封装是保证软件部件具有优良的模块性的基础,封装的目标就是要实现软件部件的“高内聚、低耦合”,防止程序相互依赖性而带来的变动影响。 - 在面向对象的编程语言中,对象是封装的最基本单位,面向对象的封装比传统语言的封装更为清晰、更为有力。 - 面向对象的封装就是把描述一个对象的属性和行为的代码封装在一个“模块”中,也就是一个类中,属性用变量定义,行为用方法进行定义, - 方法可以直接访问同一个对象中的属性。通常情况下,只要记住让变量和访问这个变量的方法放在一起,将一个类中的成员变量全部定义成私有的, - 只有这个类自己的方法才可以访问到这些成员变量,这就基本上实现对象的封装,就很容易找出要分配到这个类上的方法了, - 就基本上算是会面向对象的编程了。把握一个原则:把对同一事物进行操作的方法和相关的方法放在同一个类中,把方法和它操作的数据放在同一个类中。 - 例如,人要在黑板上画圆,这一共涉及三个对象:人、黑板、圆,画圆的方法要分配给哪个对象呢?由于画圆需要使用到圆心和半径,圆心和半径显然是圆的属性, - 如果将它们在类中定义成了私有的成员变量,那么,画圆的方法必须分配给圆,它才能访问到圆心和半径这两个属性,人以后只是调用圆的画圆方法、 - 表示给圆发给消息而已,画圆这个方法不应该分配在人这个对象上,这就是面向对象的封装性,即将对象封装成一个高度自治和相对封闭的个体, - 对象状态(属性)由这个对象自己的行为(方法)来读取和改变。一个更便于理解的例子就是,司机将火车刹住了,刹车的动作是分配给司机, - 还是分配给火车,显然,应该分配给火车,因为司机自身是不可能有那么大的力气将一个火车给停下来的,只有火车自己才能完成这一动作, - 火车需要调用内部的离合器和刹车片等多个器件协作才能完成刹车这个动作,司机刹车的过程只是给火车发了一个消息,通知火车要执行刹车动作而已。 - - - 继承: 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。 - 在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容, - 或修改原来的方法使之更适合特殊的需要,这就是继承。继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了软件的可重用性和可扩展性。 - - - 多态: 一个对象在程序不同运行时刻代表的多种状态,父类或者接口的引用指向子类对象 - 多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定, - 即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - 因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变, - 即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。多态性增强了软件的灵活性和扩展性。 - 例如,下面代码中的UserDao是一个接口,它定义引用变量userDao指向的实例对象由daofactory.getDao()在执行的时候返回,有时候指向的是UserJdbcDao这个实现, - 有时候指向的是UserHibernateDao这个实现,这样,不用修改源代码,就可以改变userDao指向的具体类实现, - 从而导致userDao.insertUser()方法调用的具体代码也随之改变,即有时候调用的是UserJdbcDao的insertUser方法, - 有时候调用的是UserHibernateDao的insertUser方法: - ```java - UserDao userDao = daofactory.getDao(); - userDao.insertUser(user); - ``` - 比喻:人吃饭,你看到的是左手,还是右手? - - 面向对象设计的重要经验: - **谁拥有数据,谁就对外提供操作这些数据的方法。** - 如: - - 人在黑板上画圆: - 画圆需要知道圆心的位置和圆的半径,而圆心的位置和圆的半径只有圆自己最清楚,所以画圆的方法是圆的方法。 - - - 列车司机紧急刹车: - 刹车的方法是司机的还是车的呢?同样,具体刹车的动作、怎么刹车只有车是清楚的,人只是发个刹车的信号给车,所以刹车的方法是车的 - - 面向对象的面试题: - - 两块石头磨成一把石刀,石刀可以将树看成木材,木材可以做出椅子: - 石头磨成石刀是哪个对象的方法呢?如果是石头内部的方法的话,那么石头磨成石刀,石头自己都没了,所以石头磨成石刀不是石头内部的方法, - 故石头变成石刀应该是一个石头加工厂的方法,将石头磨成石刀。石刀将树看成木材,所以石刀应该有一个将树看成木材的方法 - 木材可以做成椅子,所以应该有一个木材加工厂,该加工厂有一个接收木材然后加工成椅子的方法。 - - 球从一根绳子的一端移动到另一端: - 球这个对象应该有移动到下一个点的方法 - 绳子这个对象应该有提供下个点是哪个点的方法,以通知小球移动的方向 - -21. java中实现多态的机制是什么? - 靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法, - 也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。 -22. abstract class和interface有什么区别? - 含有abstract修饰符的class即为抽象类,abstract类不能创建的实例对象。含有abstract方法的类必须定义为abstract class,abstract class类中的方法不必是抽象的。 - abstract class类中定义抽象方法必须在具体(Concrete)子类中实现,所以,不能有抽象构造方法或抽象静态方法。如果的子类没有实现抽象父类中的所有抽象方法, - 那么子类也必须定义为abstract类型。接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。 - 接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final。 - 下面比较一下两者的语法区别: - - 抽象类可以有构造方法,接口中不能有构造方法。 - - 抽象类中可以有普通成员变量,接口中没有普通成员变量 - - 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。 - - 抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的, - 并且默认即为public abstract类型。 - - 抽象类中可以包含静态方法,接口中不能包含静态方法 - - 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。 - - 一个类可以实现多个接口,但只能继承一个抽象类。 -23. abstract的method是否可同时是static,是否可同时是native,是否可同时是synchronized? - abstract的method 不可以是static的,因为抽象的方法是要被子类实现的,而static与子类扯不上关系! - native方法表示该方法要用另外一种依赖平台的编程语言实现的,不存在着被子类实现的问题,所以,它也不能是抽象的,不能与abstract混用。 - 例如,FileOutputSteam类要硬件打交道,底层的实现用的是操作系统相关的api实现,例如,在windows用c语言实现的,所以,查看jdk 的源代码, - 可以发现FileOutputStream的open方法的定义如下: - `private native void open(String name) throws FileNotFoundException;` - 如果我们要用java调用别人写的c语言函数,我们是无法直接调用的,我们需要按照java的要求写一个c语言的函数,又我们的这个c语言函数去调用别人的c语言函数。 - 由于我们的c语言函数是按java的要求来写的,我们这个c语言函数就可以与java对接上,java那边的对接方式就是定义出与我们这个c函数相对应的方法, - java中对应的方法不需要写具体的代码,但需要在前面声明native。关于synchronized与abstract合用的问题,我觉得也不行,因为在我几年的学习和开发中, - 从来没见到过这种情况,并且我觉得synchronized应该是作用在一个具体的方法上才有意义。而且,方法上的synchronized同步所使用的同步锁对象是this, - 而抽象方法上无法确定this是什么。 -24. 什么是内部类?Static Nested Class 和 Inner Class的不同。 - 内部类就是在一个类的内部定义的类,内部类中不能定义静态成员(静态成员不是对象的特性,只是为了找一个容身之处,所以需要放到一个类中而已,这么一点小事, - 你还要把它放到类内部的一个类中,过分了啊!提供内部类,不是为让你干这种事情,无聊,不让你干。我想可能是既然静态成员类似c语言的全局变量, - 而内部类通常是用于创建内部对象用的,所以,把“全局变量”放在内部类中就是毫无意义的事情,既然是毫无意义的事情,就应该被禁止), - 部类可以直接访问外部类中的成员变量,内部类可以定义在外部类的方法外面,也可以定义在外部类的方法体中,如下所示: - ```java - public class Outer { - int out_x = 0; - public void method() - { - Inner1 inner1 = new Inner1(); - public class Inner2 //在方法体内部定义的内部类 - { - public method() - { - out_x = 3; - } - } - Inner2 inner2 = new Inner2(); - } - - public class Inner1 //在方法体外面定义的内部类 - { - } - } - ``` - 在方法体外面定义的内部类的访问类型可以是public,protecte,默认的,private等4种类型,这就好像类中定义的成员变量有4种访问类型一样, - 它们决定这个内部类的定义对其他类是否可见;对于这种情况,我们也可以在外面创建内部类的实例对象,创建内部类的实例对象时, - 一定要先创建外部类的实例对象,然后用这个外部类的实例对象去创建内部类的实例对象,代码如下: - ```java - Outer outer = new Outer(); - Outer.Inner1 inner1 = outer.new Innner1(); - ``` - 在方法内部定义的内部类前面不能有访问类型修饰符,就好像方法中定义的局部变量一样,但这种内部类的前面可以使用final或abstract修饰符。 - 这种内部类对其他类是不可见的其他类无法引用这种内部类,但是这种内部类创建的实例对象可以传递给其他类访问。这种内部类必须是先定义, - 后使用,即内部类的定义代码必须出现在使用该类之前,这与方法中的局部变量必须先定义后使用的道理也是一样的。这种内部类可以访问方法体中的局部变量, - 但是,该局部变量前必须加final修饰符。 -25. 内部类可以引用它的包含类的成员吗?有没有什么限制? - 完全可以。如果不是静态内部类,那没有什么限制! - 如果你把静态嵌套类当作内部类的一种特例,那在这种情况下不可以访问外部类的普通成员变量,而只能访问外部类中的静态成员,例如,下面的代码: - ```java - class Outer { - static int x; - static class Inner - { - void test() - { - syso(x); - } - } - } - ``` - 答题时,也要能察言观色,揣摩提问者的心思,显然人家希望你说的是静态内部类不能访问外部类的成员,但你一上来就顶牛,这不好,要先顺着人家, - 让人家满意,然后再说特殊情况,让人家吃惊。 -26. Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可以implements(实现)interface(接口)? - 可以继承其他类或实现其他接口。不仅是可以,而是必须! -27. super.getClass()方法调用 - 下面程序的输出结果是多少? - ```java - import java.util.Date; - public class Test extends Date{ - public static void main(String[] args) { - new Test().test(); - } - - public void test(){ - System.out.println(super.getClass().getName()); - } - } - ``` - 很奇怪,结果是Test - 这属于脑筋急转弯的题目,在一个qq群有个网友正好问过这个问题,我觉得挺有趣,就研究了一下,没想到今天还被你面到了,哈哈。 - 在test方法中,直接调用getClass().getName()方法,返回的是Test类名 - 由于getClass()在Object类中定义成了final,子类不能覆盖该方法,所以,在 - test方法中调用getClass().getName()方法,其实就是在调用从父类继承的getClass()方法,等效于调用super.getClass().getName()方法, - 所以,super.getClass().getName()方法返回的也应该是Test。如果想得到父类的名称,应该用如下代码: - `getClass().getSuperClass().getName();` - -28. jdk中哪些类是不能继承的? - 不能继承的是类是那些用final关键字修饰的类。一般比较基本的类型或防止扩展类无意间破坏原来方法的实现的类型都应该是final的, - 在jdk中System,String,StringBuffer等都是基本类型。 -29. String是最基本的数据类型吗? - 基本数据类型包括byte、int、char、long、float、double、boolean和short。 - java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用StringBuffer类 - -30. String s = "Hello";s = s + " world!";这两行代码执行后,原始的String对象中的内容到底变了没有? - 没有。因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s原先指向一个String对象,内容是 "Hello", - 然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个 String对象, - 内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。 - 通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的内存开销。 - 因为 String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用StringBuffer类,它允许修改, - 而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。 - 同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化, - 把它设置为初始值,应当这样做: - ```java - public class Demo { - private String s; - ... - public Demo { - s = "Initial Value"; - } - ... - } - ``` - 而非 - `s = new String("Initial Value");` - 后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。 - 也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。 - 上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java认为它们代表同一个String对象。而用关键字new调用构造器,总是会创建一个新的对象, - 无论内容是否相同。至于为什么要把String类设计成不可变类,是它的用途决定的。其实不只String,很多Java标准类库中的类都是不可变的。 - 在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的, - 所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以Java标准类库还提供了一个可变版本, - 即 StringBuffer。 - -31. String s = new String("xyz");创建了几个String Object? 二者之间有什么区别? - 两个或一个,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那一个。 - New String每写一遍,就创建一个新的对象,它一句那个常量”xyz”对象的内容来创建出一个新String对象。如果以前就用过’xyz’, - 这句代表就不会创建”xyz”自己了,直接从缓冲区拿。 -32. String 和StringBuffer的区别 - JAVA平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。String类表示内容不可改变的字符串。 - 而StringBuffer类表示内容可以被修改的字符串。当你知道字符数据要改变的时候你就可以使用StringBuffer。典型地,你可以使用StringBuffers来动态构造字符数据。 - 另外,String实现了equals方法,new String(“abc”).equals(new String(“abc”)的结果为true,而StringBuffer没有实现equals方法, - 所以,new StringBuffer(“abc”).equals(new StringBuffer(“abc”)的结果为false。 - 接着要举一个具体的例子来说明,我们要把1到100的所有数字拼起来,组成一个串。 - ```java - StringBuffer sbf = new StringBuffer(); - for(int i=0;i<100;i++) { - sbf.append(i); - } - ``` - 上面的代码效率很高,因为只创建了一个StringBuffer对象,而下面的代码效率很低,因为创建了101个对象。 - ```java - String str = new String(); - for(int i=0;i<100;i++) { - str = str + i; - } - ``` - 在讲两者区别时,应把循环的次数搞成10000,然后用endTime-beginTime来比较两者执行的时间差异,最后还要讲讲StringBuilder与StringBuffer的区别。 - String覆盖了equals方法和hashCode方法,而StringBuffer没有覆盖equals方法和hashCode方法,所以,将StringBuffer对象存储进Java集合类中时会出现问题。 - -33. StringBuffer与StringBuilder的区别 - StringBuffer和StringBuilder类都表示内容可以被修改的字符串,StringBuilder是线程不安全的,运行效率高,如果一个字符串变量是在方法里面定义, - 这种情况只可能有一个线程访问它,不存在不安全的因素了,则用StringBuilder。如果要在类里面定义成员变量,并且这个类的实例对象会在多线程环境下使用, - 那么最好用StringBuffer。 - -34. 如何把一段逗号分割的字符串转换成一个数组? - 如果不查jdk api,我很难写出来!我可以说说我的思路: - - 用正则表达式,代码大概为:String [] result = orgStr.split(“,”); - - 用 StingTokenizer ,代码为: - ```java - StringTokenizer tokener = StringTokenizer(orgStr,”,”); - String [] result = new String[tokener .countTokens()]; - Int i=0; - while(tokener.hasNext(){ - result[i++]=toker.nextToken(); - } - ``` -35. 数组有没有length()这个方法? String有没有length()这个方法? - 数组没有length()这个方法,有length的属性。String有length()这个方法。 - -36. 下面这条语句一共创建了多少个对象:String s="a"+"b"+"c"+"d"; - 答:对于如下代码: - String s1 = "a"; - String s2 = s1 + "b"; - String s3 = "a" + "b"; - System.out.println(s2 == "ab"); - System.out.println(s3 == "ab"); - 第一条语句打印的结果为false,第二条语句打印的结果为true,这说明javac编译可以对字符串常量直接相加的表达式进行优化, - 不必要等到运行期去进行加法运算处理,而是在编译时去掉其中的加号,直接将其编译成一个这些常量相连的结果。 - 题目中的第一行代码被编译器在编译时优化后,相当于直接定义了一个”abcd”的字符串,所以,上面的代码应该只创建了一个String对象。写如下两行代码, - ```java - String s = "a" + "b" + "c" + "d"; - System.out.println(s == "abcd"); - ``` - 最终打印的结果应该为true。 - -37. try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后? - 也许你的答案是在return之前,但往更细地说,我的答案是在return中间执行,请看下面程序代码的运行结果: - ```java - public class Test { - - /** - * @param args add by zxx ,Dec 9, 2008 - */ - public static void main(String[] args) { - // TODO Auto-generated method stub - System.out.println(new Test().test());; - } - - static int test() - { - int x = 1; - try - { - return x; - } - finally - { - ++x; - } - } - - } - ``` - ---------执行结果 --------- - 1 - - 运行结果是1,为什么呢?主函数调用子函数并得到结果的过程,好比主函数准备一个空罐子,当子函数要返回结果时,先把结果放在罐子里, - 然后再将程序逻辑返回到主函数。所谓返回,就是子函数说,我不运行了,你主函数继续运行吧,这没什么结果可言,结果是在说这话之前放进罐子里的。 -38. 下面的程序代码输出的结果是多少? - ```java - public class smallT { - public static void main(String args[]) { - smallT t = new smallT(); - int b = t.get(); - System.out.println(b); - } - - public int get() { - try - { - return 1 ; - } - finally - { - return 2 ; - } - } - } - - 返回的结果是2。 - 我可以通过下面一个例子程序来帮助我解释这个答案,从下面例子的运行结果中可以发现,try中的return语句调用的函数先于finally中调用的函数执行, - 也就是说return语句先执行,finally语句后执行,所以,返回的结果是2。Return并不是让函数马上返回,而是return语句执行后,将把返回结果放置进函数栈中, - 此时函数并不是马上返回,它要执行finally语句后才真正开始返回。 - 在讲解答案时可以用下面的程序来帮助分析: - ```java - public class Test { - /** - * @param args add by zxx ,Dec 9, 2008 - */ - public static void main(String[] args) { - // TODO Auto-generated method stub - System.out.println(new Test().test());; - } - - int test() { - try - { - return func1(); - } - finally - { - return func2(); - } - } - - int func1() { - System.out.println("func1"); - return 1; - } - int func2() { - System.out.println("func2"); - return 2; - } - } - -----------执行结果----------------- - - func1 - func2 - 2 - ``` - 结论:finally中的代码比return 和break语句后执行 - -39. final, finally, finalize的区别。 - final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 - 内部类要访问局部变量,局部变量必须定义成final类型. - finally是异常处理语句结构的一部分,表示总是执行。 - finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。 - JVM不保证此方法总被调用 - -40. 运行时异常与一般异常有何异同? - 异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。 - java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。 -41. error和exception有什么区别? - error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。 - exception 表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。 - -42. java中有几种方法可以实现一个线程?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用? - java5以前,有如下两种: - 第一种: - ```java - new Thread(){}.start();这表示调用Thread子类对象的run方法,new Thread(){}表示一个Thread的匿名子类的实例对象,子类加上run方法后的代码如下: - new Thread(){ - public void run(){ - } - }.start(); - - 第二种: - ```java - new Thread(new Runnable(){}).start();这表示调用Thread对象接受的Runnable对象的run方法,new Runnable(){}表示一个Runnable的匿名子类的实例对象,runnable的子类加上run方法后的代码如下: - new Thread(new Runnable(){ - public void run(){ - } - } - ).start(); - - ``` - 从java5开始,还有如下一些线程池创建多线程的方式: - ```java - ExecutorService pool = Executors.newFixedThreadPool(3) - for(int i=0;i<10;i++) { - pool.execute(new Runable(){public void run(){}}); - } - Executors.newCachedThreadPool().execute(new Runable(){public void run(){}}); - Executors.newSingleThreadExecutor().execute(new Runable(){public void run(){}}); - ``` - - 两种实现方法,分别是继承Thread类与实现Runnable接口 - 用synchronized关键字修饰同步方法 - 反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。 - 结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。 - 此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源, - 就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。 - 若标志指出线程应当恢复,则用一个notify()重新启动线程。 - -43. 启动一个线程是用run()还是start()? - 启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。 - -44. 简述synchronized和java.util.concurrent.locks.Lock的异同 ? - 主要相同点:Lock能完成synchronized所实现的所有功能 - 主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放, - 并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。 - -45. 设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 - 以下程序使用内部类实现线程,对j增减的时候没有考虑顺序问题。 - ```java - public class ThreadTest1 - { - private int j; - public static void main(String args[]){ - ThreadTest1 tt=new ThreadTest1(); - Inc inc=tt.new Inc(); - Dec dec=tt.new Dec(); - for(int i=0;i<2;i++){ - Thread t=new Thread(inc); - t.start(); - t=new Thread(dec); - t.start(); - } - } - private synchronized void inc(){ - j++; - System.out.println(Thread.currentThread().getName()+"-inc:"+j); - } - private synchronized void dec(){ - j--; - System.out.println(Thread.currentThread().getName()+"-dec:"+j); - } - class Inc implements Runnable{ - public void run(){ - for(int i=0;i<100;i++){ - inc(); - } - } - } - class Dec implements Runnable{ - public void run(){ - for(int i=0;i<100;i++){ - dec(); - } - } - } - } - - ----------随手再写的一个------------- - class A - { - JManger j =new JManager(); - main() - { - new A().call(); - } - - void call - { - for(int i=0;i<2;i++) - { - new Thread( - new Runnable(){ public void run(){while(true){j.accumulate()}}} - ).start(); - new Thread(new Runnable(){ public void run(){while(true){j.sub()}}}).start(); - } - } - } - - class JManager - { - private j = 0; - - public synchronized void subtract() - { - j-- - } - - public synchronized void accumulate() - { - j++; - } - - } - ``` - -46. 子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。 - ```java - public class ThreadTest { - - /** - * @param args - */ - public static void main(String[] args) { - // TODO Auto-generated method stub - new ThreadTest().init(); - } - - public void init() - { - final Business business = new Business(); - new Thread( - new Runnable() - { - - public void run() { - for(int i=0;i<50;i++) - { - business.SubThread(i); - } - } - - } - - ).start(); - - for(int i=0;i<50;i++) - { - business.MainThread(i); - } - } - - private class Business - { - boolean bShouldSub = true;//这里相当于定义了控制该谁执行的一个信号灯 - public synchronized void MainThread(int i) - { - if(bShouldSub) - try { - this.wait(); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - for(int j=0;j<5;j++) - { - System.out.println(Thread.currentThread().getName() + ":i=" + i +",j=" + j); - } - bShouldSub = true; - this.notify(); - } - - public synchronized void SubThread(int i) - { - if(!bShouldSub) - try { - this.wait(); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - for(int j=0;j<10;j++) - { - System.out.println(Thread.currentThread().getName() + ":i=" + i +",j=" + j); - } - bShouldSub = false; - this.notify(); - } - } - } - - 备注:不可能一上来就写出上面的完整代码,最初写出来的代码如下,问题在于两个线程的代码要参照同一个变量,即这两个线程的代码要共享数据, - 所以,把这两个线程的执行代码搬到同一个类中去: - - package com.huawei.interview.lym; - - public class ThreadTest { - - private static boolean bShouldMain = false; - - public static void main(String[] args) { - // TODO Auto-generated method stub - /*new Thread(){ - public void run() - { - for(int i=0;i<50;i++) - { - for(int j=0;j<10;j++) - { - System.out.println("i=" + i + ",j=" + j); - } - } - } - - }.start();*/ - - //final String str = new String(""); - new Thread( - new Runnable() - { - public void run() - { - for(int i=0;i<50;i++) - { - synchronized (ThreadTest.class) { - if(bShouldMain) - { - try { - ThreadTest.class.wait();} - catch (InterruptedException e) { - e.printStackTrace(); - } - } - for(int j=0;j<10;j++) - { - System.out.println( - Thread.currentThread().getName() + - "i=" + i + ",j=" + j); - } - bShouldMain = true; - ThreadTest.class.notify(); - } - } - } - } - ).start(); - - for(int i=0;i<50;i++) - { - synchronized (ThreadTest.class) { - if(!bShouldMain) - { - try { - ThreadTest.class.wait();} - catch (InterruptedException e) { - e.printStackTrace(); - } - } - for(int j=0;j<5;j++) - { - System.out.println( - Thread.currentThread().getName() + - "i=" + i + ",j=" + j); - } - bShouldMain = false; - ThreadTest.class.notify(); - } - } - } - } - 下面使用jdk5中的并发库来实现的: - import java.util.concurrent.Executors; - import java.util.concurrent.ExecutorService; - import java.util.concurrent.locks.Lock; - import java.util.concurrent.locks.ReentrantLock; - import java.util.concurrent.locks.Condition; - - public class ThreadTest - { - private static Lock lock = new ReentrantLock(); - private static Condition subThreadCondition = lock.newCondition(); - private static boolean bBhouldSubThread = false; - public static void main(String [] args) - { - ExecutorService threadPool = Executors.newFixedThreadPool(3); - threadPool.execute(new Runnable(){ - public void run() - { - for(int i=0;i<50;i++) - { - lock.lock(); - try - { - if(!bBhouldSubThread) - subThreadCondition.await(); - for(int j=0;j<10;j++) - { - System.out.println(Thread.currentThread().getName() + ",j=" + j); - } - bBhouldSubThread = false; - subThreadCondition.signal(); - }catch(Exception e) - { - } - finally - { - lock.unlock(); - } - } - } - - }); - threadPool.shutdown(); - for(int i=0;i<50;i++) - { - lock.lock(); - try - { - if(bBhouldSubThread) - subThreadCondition.await(); - for(int j=0;j<10;j++) - { - System.out.println(Thread.currentThread().getName() + ",j=" + j); - } - bBhouldSubThread = true; - subThreadCondition.signal(); - }catch(Exception e) - { - } - finally - { - lock.unlock(); - } - } - } - } - ``` - -47. ArrayList和Vector的区别 - 这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组, - 我们以后可以按位置索引号取出某个元素,,并且其中的数据是允许重复的,这是HashSet之类的集合的最大不同处,HashSet之类的集合不可以按索引号去检索其中的元素, - 也不允许有重复的元素(本来题目问的与hashset没有任何关系,但为了说清楚ArrayList与Vector的功能,我们使用对比方式,更有利于说明问题)。 - 接着才说ArrayList与Vector的区别,这主要包括两个方面: - - 同步性: - Vector是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList是线程序不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合, - 那最好是使用ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们自己再去考虑和编写线程安全的代码。 - 备注:对于Vector&ArrayList、Hashtable&HashMap,要记住线程安全的问题,记住Vector与Hashtable是旧的,是java一诞生就提供了的,它们是线程安全的, - ArrayList与HashMap是java2时才提供的,它们是线程不安全的。所以,我们讲课时先讲老的。 - - 数据增长: - ArrayList与Vector都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList与Vector的存储空间,每次要增加存储空间时, - 不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector默认增长为原来两倍, - 而ArrayList的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍)。ArrayList与Vector都可以设置初始的空间大小, - Vector还可以设置增长的空间大小,而ArrayList没有提供设置增长空间的方法。 - 总结:即Vector增长原来的一倍,ArrayList增加原来的0.5倍。 - -48. HashMap和Hashtable的区别 - HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全, - 在只有一个线程访问的情况下,效率要高于Hashtable。 - HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。 - HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。 - Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。 - 最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。 - Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。 - 就HashMap与HashTable主要从三方面来说。 - - 历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现 - - 同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的 - - 值:只有HashMap可以让你将空值作为一个表的条目的key或value - -49. List 和 Map 区别? - 一个是存储单列数据的集合,另一个是存储键和值这样的双列数据的集合,List中存储的数据是有顺序,并且允许重复;Map中存储的数据是没有顺序的, - 其键是不能重复的,它的值是可以有重复的。 - -50. List, Set, Map是否继承自Collection接口? - List,Set是,Map不是 - -51. List、Map、Set三个接口,存取元素时,各有什么特点? - 这样的题属于随意发挥题:这样的题比较考水平,两个方面的水平:一是要真正明白这些内容,二是要有较强的总结和表述能力。如果你明白, - 但表述不清楚,在别人那里则等同于不明白。 - 首先,List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个功共同的父接口,叫Collection。Set里面不允许有重复的元素,所谓重复, - 即不能有两个相等(注意,不是仅仅是相同)的对象 ,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等, - 则B对象存储不进去,所以,Set集合的add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true, - 当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。Set取元素时,没法说取第几个,只能以Iterator接口取得所有的元素, - 再逐一遍历各个元素。List表示有先后顺序的集合, 注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obj e)方法时, - 每次加入的对象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用add(int index,Obj e)方法, - 就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List中,每调用一次add方法,这个对象就被插入进集合中一次, - 其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个索引指向了这个对象, - 如图x所示。List除了可以以Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以调用get(index i)来明确说明取第几个。 - Map与List和Set不同,它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key, - 这个重复的规则也是按equals比较相等。取则可以根据key获得相应的value,即get(Object key)返回值为key 所对应的value。另外,也可以获得所有的key的结合, - 还可以获得所有的value的结合,还可以获得key和value组合成的Map.Entry对象的集合。 - List 以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,内部排序。Map 保存key-value值,value可多值。 - HashSet按照hashcode值的某种运算方式进行存储,而不是直接按hashCode值的大小进行存储。 - 例如,"abc" ---> 78,"def" ---> 62,"xyz" ---> 65在hashSet中的存储顺序不是62,65,78,LinkedHashSet按插入的顺序存储, - 那被存储对象的hashcode方法还有什么作用呢?学员想想!hashset集合比较两个对象是否相等,首先看hashcode方法是否相等,然后看equals方法是否相等。 - new 两个Student插入到HashSet中,看HashSet的size,实现hashcode和equals方法后再看size。 - 同一个对象可以在Vector中加入多次。往集合里面加元素,相当于集合里用一根绳子连接到了目标对象。往HashSet中却加不了多次的。 - -52. 说出ArrayList,Vector, LinkedList的存储性能和特性 - ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素, - 但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差, - 而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。 - LinkedList也是线程不安全的,LinkedList提供了一些方法,使得LinkedList可以被当作堆栈和队列来使用。 - -53. 去掉一个Vector集合中重复的元素 - ```java - Vector newVector = new Vector(); - For (int i=0;i1 ){ - regex = "" + seperators[0] + "|" + seperators[1]; - }else{ - regex = "" + seperators[0]; - } - words = results.split(regex); - } - - public String nextWord(){ - if(pos == words.length) - return null; - return words[pos++]; - } - } - ``` - -3. 编写一个程序,将d:\java目录下的所有.java文件复制到d:\jad目录下,并将原来文件的扩展名从.java改为.jad。 - listFiles方法接受一个FileFilter对象,这个FileFilter对象就是过虑的策略对象,不同的人提供不同的FileFilter实现,即提供了不同的过滤策略。 - ```java - import java.io.File; - import java.io.FileInputStream; - import java.io.FileOutputStream; - import java.io.FilenameFilter; - import java.io.IOException; - import java.io.InputStream; - import java.io.OutputStream; - - public class Jad2Java { - - public static void main(String[] args) throws Exception { - File srcDir = new File("java"); - if(!(srcDir.exists() && srcDir.isDirectory())) - throw new Exception("目录不存在"); - File[] files = srcDir.listFiles( - new FilenameFilter(){ - - public boolean accept(File dir, String name) { - return name.endsWith(".java"); - } - - } - ); - - System.out.println(files.length); - File destDir = new File("jad"); - if(!destDir.exists()) destDir.mkdir(); - for(File f :files){ - FileInputStream fis = new FileInputStream(f); - String destFileName = f.getName().replaceAll("\\.java$", ".jad"); - FileOutputStream fos = new FileOutputStream(new File(destDir,destFileName)); - copy(fis,fos); - fis.close(); - fos.close(); - } - } - - private static void copy(InputStream ips,OutputStream ops) throws Exception{ - int len = 0; - byte[] buf = new byte[1024]; - while((len = ips.read(buf)) != -1){ - ops.write(buf,0,len); - } - } - ``` - 由本题总结的思想及策略模式的解析: - 1. - ```java - class jad2java{ - 1. 得到某个目录下的所有的java文件集合 - 1.1 得到目录 File srcDir = new File("d:\\java"); - 1.2 得到目录下的所有java文件:File[] files = srcDir.listFiles(new MyFileFilter()); - 1.3 只想得到.java的文件: class MyFileFilter implememyts FileFilter{ - public boolean accept(File pathname){ - return pathname.getName().endsWith(".java") - } - } - - 2.将每个文件复制到另外一个目录,并改扩展名 - 2.1 得到目标目录,如果目标目录不存在,则创建之 - 2.2 根据源文件名得到目标文件名,注意要用正则表达式,注意.的转义。 - 2.3 根据表示目录的File和目标文件名的字符串,得到表示目标文件的File。 - //要在硬盘中准确地创建出一个文件,需要知道文件名和文件的目录。 - 2.4 将源文件的流拷贝成目标文件流,拷贝方法独立成为一个方法,方法的参数采用抽象流的形式。 - //方法接受的参数类型尽量面向父类,越抽象越好,这样适应面更宽广。 - } - ``` - 分析listFiles方法内部的策略模式实现原理 - ```java - File[] listFiles(FileFilter filter){ - File[] files = listFiles(); - //Arraylist acceptedFilesList = new ArrayList(); - File[] acceptedFiles = new File[files.length]; - int pos = 0; - for(File file: files){ - boolean accepted = filter.accept(file); - if(accepted){ - //acceptedFilesList.add(file); - acceptedFiles[pos++] = file; - } - } - - Arrays.copyOf(acceptedFiles,pos); - //return (File[])accpetedFilesList.toArray(); - } - ``` - -4. 编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串,但要保证汉字不被截取半个,如“我ABC”,4,应该截取“我AB”, - 输入“我ABC汉DEF”,6,应该输出“我ABC”,而不是“我ABC+汉的半个”。 - ```java - 首先要了解中文字符有多种编码及各种编码的特征。 - 假设n为要截取的字节数。 - public static void main(String[] args) throws Exception{ - String str = "我a爱中华abc我爱传智def'; - String str = "我ABC汉"; - int num = trimGBK(str.getBytes("GBK"),5); - System.out.println(str.substring(0,num) ); - } - - public static int trimGBK(byte[] buf,int n){ - int num = 0; - boolean bChineseFirstHalf = false; - for(int i=0;i=’0’ && ch<=’9’) { - digitCount++ - } else if((ch>=’a’ && ch<=’z’) || (ch>=’A’ && ch<=’Z’)) { - engishCount++; - } else { - chineseCount++; - } - } - System.out.println(……………); - ``` - -6. 说明生活中遇到的二叉树,用java实现二叉树 - 这是组合设计模式。 - 我有很多个(假设10万个)数据要保存起来,以后还需要从保存的这些数据中检索是否存在某个数据,(我想说出二叉树的好处,该怎么说呢?那就是说别人的缺点), - 假如存在数组中,那么,碰巧要找的数字位于99999那个地方,那查找的速度将很慢,因为要从第1个依次往后取,取出来后进行比较。 - 平衡二叉树(构建平衡二叉树需要先排序,我们这里就不作考虑了)可以很好地解决这个问题,但二叉树的遍历(前序,中序,后序)效率要比数组低很多, - 代码如下: - ```java - package com.huawei.interview; - - public class Node { - public int value; - public Node left; - public Node right; - - public void store(int value) { - if(valuethis.value) { - if(right == null) { - right = new Node(); - right.value=value; - } else { - right.store(value); - } - } - } - - public boolean find(int value) { - System.out.println("happen " + this.value); - if (value == this.value) { - return true; - } else if(value>this.value) { - if(right == null) return false; - return right.find(value); - } else { - if(left == null) return false; - return left.find(value); - } - } - - public void preList() { - System.out.print(this.value + ","); - if(left!=null) left.preList(); - if(right!=null) right.preList(); - } - - public void middleList() { - if(left!=null) left.preList(); - System.out.print(this.value + ","); - if(right!=null) right.preList(); - } - public void afterList() { - if(left!=null) left.preList(); - if(right!=null) right.preList(); - System.out.print(this.value + ","); - } - public static void main(String [] args) { - int [] data = new int[20]; - for(int i=0;i this.value) { - if(right != null) { - right.add(value); - } else { - Node node = new Node(value); - right = node; - } - } else { - if(left != null) { - left.add(value); - } else { - Node node = new Node(value); - left = node; - } - } - } - - public boolean find(int value){ - if(value == this.value) { - return true; - } else if(value > this.value){ - if(right == null) return false; - else return right.find(value); - }else{ - if(left == null) return false; - else return left.find(value); - } - } - - public void display(){ - System.out.println(value); - if(left != null) left.display(); - if(right != null) right.display(); - } - - /*public Iterator iterator(){ - - }*/ - - public static void main(String[] args){ - int[] values = new int[8]; - for(int i=0;i<8;i++){ - int num = (int)(Math.random() * 15); - //System.out.println(num); - //if(Arrays.binarySearch(values, num)<0) - if(!contains(values,num)) - values[i] = num; - else - i--; - } - - System.out.println(Arrays.toString(values)); - - Node root = new Node(values[0]); - for(int i=1;iuser2.value) - { - return 1; - }else - { - return user1.name.compareTo(user2.name); - } - } - - } - ); - Iterator iterator = results.keySet().iterator(); - while(iterator.hasNext()) - { - String name = (String)iterator.next(); - Integer value = (Integer)results.get(name); - if(value > 1) - { - sortedResults.add(new User(name,value)); - } - } - - printResults(sortedResults); - } - private static void printResults(TreeSet sortedResults) - { - Iterator iterator = sortedResults.iterator(); - while(iterator.hasNext()) - { - User user = (User)iterator.next(); - System.out.println(user.name + ":" + user.value); - } - } - public static void dealLine(String line,Map map) - { - if(!"".equals(line.trim())) - { - String [] results = line.split(","); - if(results.length == 3) - { - String name = results[1]; - Integer value = (Integer)map.get(name); - if(value == null) value = 0; - map.put(name,value + 1); - } - } - } - - } - ``` - -8. 写一个Singleton出来。 - Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。 - - 饱汉模式 - ```java - public class SingleTon { - private SingleTon(){ - } - - //实例化放在静态代码块里可提高程序的执行效率,但也可能更占用空间 - private final static SingleTon instance = new SingleTon(); - public static SingleTon getInstance(){ - return instance; - } - } - ``` - - 饿汉模式 - ```java - public class SingleTon { - private SingleTon(){} - - private static instance = null;//new SingleTon(); - - public static synchronized SingleTon getInstance(){ - if(instance == null) - instance = new SingleTon(); - return instance; - } - } - ``` - - 用枚举 - ```java - public enum SingleTon{ - ONE; - - } - ``` - -9. 递归算法题 - 一个整数,大于0,不用循环和本地变量,按照n,2n,4n,8n的顺序递增,当值大于5000时,把值按照指定顺序输出来。 - 例:n=1237 - 则输出为: - 1237, - 2474, - 4948, - 9896, - 9896, - 4948, - 2474, - 1237, - 提示:写程序时,先只写按递增方式的代码,写好递增的以后,再增加考虑递减部分。 - ```java - public static void doubleNum(int n) { - System.out.println(n); - if(n<=5000) - doubleNum(n*2); - System.out.println(n); - } - ``` - -10. 递归算法题 - 第1个人10,第2个比第1个人大2岁,依次递推,请用递归方式计算出第8个人多大? - ```java - package cn.itcast; - - import java.util.Date; - - public class A1 { - - public static void main(String [] args) { - System.out.println(computeAge(8)); - } - - public static int computeAge(int n) { - if(n==1) return 10; - return computeAge(n-1) + 2; - } - } - - public static void toBinary(int n,StringBuffer result) { - if(n/2 != 0) - toBinary(n/2,result); - result.append(n%2); - } - ``` - -11. 排序都有哪几种方法?请列举。用JAVA实现一个快速排序。 - 本人只研究过冒泡排序、选择排序和快速排序,下面是快速排序的代码: - ```java - public class QuickSort { - /** - * 快速排序 - * @param strDate - * @param left - * @param right - */ - public void quickSort(String[] strDate,int left,int right){ - String middle,tempDate; - int i,j; - i=left; - j=right; - middle=strDate[(i+j)/2]; - do{ - while(strDate[i].compareTo(middle)<0&& i0&& j>left) - j--; //找出右边比中间值小的数 - if(i<=j){ //将左边大的数和右边小的数进行替换 - tempDate=strDate[i]; - strDate[i]=strDate[j]; - strDate[j]=tempDate; - i++; - j--; - } - }while(i<=j); //当两者交错6时停止 - - if(ileft){ - quickSort(strDate,left,j); - } - } - /** - * @param args - */ - public static void main(String[] args){ - String[] strVoid=new String[]{"11","66","22","0","55","22","0","32"}; - QuickSort sort=new QuickSort(); - sort.quickSort(strVoid,0,strVoid.length-1); - for(int i=0;i(一千零一拾一元整)输出。 - ```java - // 去零的代码: - return sb.reverse().toString().replaceAll("零[拾佰仟]","零").replaceAll("零+万","万").replaceAll("零+元","元").replaceAll("零+","零"); - - public class RenMingBi { - - /** - * @param args add by zxx ,Nov 29, 2008 - */ - private static final char[] data = new char[]{ - '零','壹','贰','叁','肆','伍','陆','柒','捌','玖' - }; - private static final char[] units = new char[]{ - '元','拾','佰','仟','万','拾','佰','仟','亿' - }; - public static void main(String[] args) { - // TODO Auto-generated method stub - System.out.println( - convert(135689123)); - } - - public static String convert(int money) { - StringBuffer sbf = new StringBuffer(); - int unit = 0; - while(money!=0) { - sbf.insert(0,units[unit++]); - int number = money%10; - sbf.insert(0, data[number]); - money /= 10; - } - - return sbf.toString(); - } - } - ``` - ----- -- 邮箱 :charon.chui@gmail.com +Java基础面试题 +=== + +本部分全部内容是根据张孝祥老师的Word文档整理而来。只不过是为了方便观看,把代码部分用`markdown`来展示。整理时脑海中不断回忆起张老师上课的情景,真是怀念。 + +1. 一个`.java`源文件中是否可以包括多个类(不是内部类)?有什么限制? + 可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。 +2. `Java`有没有`goto`? + `java`中的保留字,现在没有在`java`中使用。 +3. 说说`&`和`&&`的区别。 + `&`和`&&`都可以用作逻辑与的运算符,表示逻辑与`(and)`,当运算符两边的表达式的结果都为`true`时,整个运算结果才为`true`, + 否则,只要有一方为`false`,则结果为`false`。 + `&&`还具有短路的功能,即如果第一个表达式为`false`,则不再计算第二个表达式,例如,对于`if(str != null && !str.equals(“”))`表达式,当`str`为`null`时, + 后面的表达式不会执行,所以不会出现`NullPointerException`如果将`&&`改为`&`,则会抛出`NullPointerException`异常。 + +4. 在`JAVA`中如何跳出当前的多重嵌套循环? + 在`Java`中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的`break`语句,即可跳出外层循环。例如, + ```java + ok: + for(int i=0;i<10;i++) { + for(int j=0;j<10;j++) { + System.out.println(“i=” + i + “,j=” + j); + if(j == 5) break ok; + } + } + ``` + 另外,我个人通常并不使用标号这种方式,而是让外层的循环条件表达式的结果可以受到里层循环体代码的控制,例如,要在二维数组中查找到某个数字。 + ```java + int arr[][] = ...; + boolean found = false; + for(int i=0;i“zxx,male,28,30000”?Person, + 既然大家都要这么干,并且没有个统一的干法,于是,sun公司就提出一种统一的解决方案,它会把对象变成某个格式进行输入和输出, + 这种格式对程序员来说是透明(transparent)的,但是,我们的某个类要想能被sun的这种方案处理,必须实现Serializable接口。 + ObjectOutputStream.writeObject(obj); + Object obj = ObjectInputStream.readObject(); + 假设两年前我保存了某个类的一个对象,这两年来,我修改该类,删除了某个属性和增加了另外一个属性,两年后,我又去读取那个保存的对象,或有什么结果? + 未知!sun的jdk就会蒙了。为此,一个解决办法就是在类中增加版本后,每一次类的属性修改,都应该把版本号升级一下,这样,在读取时, + 比较存储对象时的版本号与当前类的版本号,如果不一致,则直接报版本号不同的错! +20. 面向对象的特征有哪些方面 + 面向对象是相对于面向过程而言的,面向过程强调的是功能,面向对象强调的是将功能封装进对象强调具备功能的对象, + - 思想特点好处: + - 是符合人们思考习惯的一种思想; + - 将复杂的事情简单化了; + - 将程序员从执行者变成了指挥者; + 注:比如我要达到某种结果我要是用某个功能,我就寻找能帮我达到该结果的功能的对象,如果该对象具备了该功能 + 那么我拿到该对象,也就可以使用该对象的功能。如我要洗衣服我就买洗衣机,至于怎么洗我不管。 + + 面向对象的编程语言有封装、继承 、多态等3个主要的特征。 + - 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式 + 封装是保证软件部件具有优良的模块性的基础,封装的目标就是要实现软件部件的“高内聚、低耦合”,防止程序相互依赖性而带来的变动影响。 + 在面向对象的编程语言中,对象是封装的最基本单位,面向对象的封装比传统语言的封装更为清晰、更为有力。 + 面向对象的封装就是把描述一个对象的属性和行为的代码封装在一个“模块”中,也就是一个类中,属性用变量定义,行为用方法进行定义, + 方法可以直接访问同一个对象中的属性。通常情况下,只要记住让变量和访问这个变量的方法放在一起,将一个类中的成员变量全部定义成私有的, + 只有这个类自己的方法才可以访问到这些成员变量,这就基本上实现对象的封装,就很容易找出要分配到这个类上的方法了, + 就基本上算是会面向对象的编程了。把握一个原则:把对同一事物进行操作的方法和相关的方法放在同一个类中,把方法和它操作的数据放在同一个类中。 + 例如,人要在黑板上画圆,这一共涉及三个对象:人、黑板、圆,画圆的方法要分配给哪个对象呢?由于画圆需要使用到圆心和半径,圆心和半径显然是圆的属性, + 如果将它们在类中定义成了私有的成员变量,那么,画圆的方法必须分配给圆,它才能访问到圆心和半径这两个属性,人以后只是调用圆的画圆方法、 + 表示给圆发给消息而已,画圆这个方法不应该分配在人这个对象上,这就是面向对象的封装性,即将对象封装成一个高度自治和相对封闭的个体, + 对象状态(属性)由这个对象自己的行为(方法)来读取和改变。一个更便于理解的例子就是,司机将火车刹住了,刹车的动作是分配给司机, + 还是分配给火车,显然,应该分配给火车,因为司机自身是不可能有那么大的力气将一个火车给停下来的,只有火车自己才能完成这一动作, + 火车需要调用内部的离合器和刹车片等多个器件协作才能完成刹车这个动作,司机刹车的过程只是给火车发了一个消息,通知火车要执行刹车动作而已。 + + - 继承: 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。 + 在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容, + 或修改原来的方法使之更适合特殊的需要,这就是继承。继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了软件的可重用性和可扩展性。 + + - 多态: 一个对象在程序不同运行时刻代表的多种状态,父类或者接口的引用指向子类对象 + 多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定, + 即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 + 因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变, + 即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。多态性增强了软件的灵活性和扩展性。 + 例如,下面代码中的UserDao是一个接口,它定义引用变量userDao指向的实例对象由daofactory.getDao()在执行的时候返回,有时候指向的是UserJdbcDao这个实现, + 有时候指向的是UserHibernateDao这个实现,这样,不用修改源代码,就可以改变userDao指向的具体类实现, + 从而导致userDao.insertUser()方法调用的具体代码也随之改变,即有时候调用的是UserJdbcDao的insertUser方法, + 有时候调用的是UserHibernateDao的insertUser方法: + ```java + UserDao userDao = daofactory.getDao(); + userDao.insertUser(user); + ``` + 比喻:人吃饭,你看到的是左手,还是右手? + + 面向对象设计的重要经验: + **谁拥有数据,谁就对外提供操作这些数据的方法。** + 如: + - 人在黑板上画圆: + 画圆需要知道圆心的位置和圆的半径,而圆心的位置和圆的半径只有圆自己最清楚,所以画圆的方法是圆的方法。 + + - 列车司机紧急刹车: + 刹车的方法是司机的还是车的呢?同样,具体刹车的动作、怎么刹车只有车是清楚的,人只是发个刹车的信号给车,所以刹车的方法是车的 + + 面向对象的面试题: + - 两块石头磨成一把石刀,石刀可以将树看成木材,木材可以做出椅子: + 石头磨成石刀是哪个对象的方法呢?如果是石头内部的方法的话,那么石头磨成石刀,石头自己都没了,所以石头磨成石刀不是石头内部的方法, + 故石头变成石刀应该是一个石头加工厂的方法,将石头磨成石刀。石刀将树看成木材,所以石刀应该有一个将树看成木材的方法 + 木材可以做成椅子,所以应该有一个木材加工厂,该加工厂有一个接收木材然后加工成椅子的方法。 + - 球从一根绳子的一端移动到另一端: + 球这个对象应该有移动到下一个点的方法 + 绳子这个对象应该有提供下个点是哪个点的方法,以通知小球移动的方向 + +21. java中实现多态的机制是什么? + 靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法, + 也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。 +22. abstract class和interface有什么区别? + 含有abstract修饰符的class即为抽象类,abstract类不能创建的实例对象。含有abstract方法的类必须定义为abstract class,abstract class类中的方法不必是抽象的。 + abstract class类中定义抽象方法必须在具体(Concrete)子类中实现,所以,不能有抽象构造方法或抽象静态方法。如果的子类没有实现抽象父类中的所有抽象方法, + 那么子类也必须定义为abstract类型。接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。 + 接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final。 + 下面比较一下两者的语法区别: + - 抽象类可以有构造方法,接口中不能有构造方法。 + - 抽象类中可以有普通成员变量,接口中没有普通成员变量 + - 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。 + - 抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的, + 并且默认即为public abstract类型。 + - 抽象类中可以包含静态方法,接口中不能包含静态方法 + - 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。 + - 一个类可以实现多个接口,但只能继承一个抽象类。 +23. abstract的method是否可同时是static,是否可同时是native,是否可同时是synchronized? + abstract的method 不可以是static的,因为抽象的方法是要被子类实现的,而static与子类扯不上关系! + native方法表示该方法要用另外一种依赖平台的编程语言实现的,不存在着被子类实现的问题,所以,它也不能是抽象的,不能与abstract混用。 + 例如,FileOutputSteam类要硬件打交道,底层的实现用的是操作系统相关的api实现,例如,在windows用c语言实现的,所以,查看jdk 的源代码, + 可以发现FileOutputStream的open方法的定义如下: + `private native void open(String name) throws FileNotFoundException;` + 如果我们要用java调用别人写的c语言函数,我们是无法直接调用的,我们需要按照java的要求写一个c语言的函数,又我们的这个c语言函数去调用别人的c语言函数。 + 由于我们的c语言函数是按java的要求来写的,我们这个c语言函数就可以与java对接上,java那边的对接方式就是定义出与我们这个c函数相对应的方法, + java中对应的方法不需要写具体的代码,但需要在前面声明native。关于synchronized与abstract合用的问题,我觉得也不行,因为在我几年的学习和开发中, + 从来没见到过这种情况,并且我觉得synchronized应该是作用在一个具体的方法上才有意义。而且,方法上的synchronized同步所使用的同步锁对象是this, + 而抽象方法上无法确定this是什么。 +24. 什么是内部类?Static Nested Class 和 Inner Class的不同。 + 内部类就是在一个类的内部定义的类,内部类中不能定义静态成员(静态成员不是对象的特性,只是为了找一个容身之处,所以需要放到一个类中而已,这么一点小事, + 你还要把它放到类内部的一个类中,过分了啊!提供内部类,不是为让你干这种事情,无聊,不让你干。我想可能是既然静态成员类似c语言的全局变量, + 而内部类通常是用于创建内部对象用的,所以,把“全局变量”放在内部类中就是毫无意义的事情,既然是毫无意义的事情,就应该被禁止), + 部类可以直接访问外部类中的成员变量,内部类可以定义在外部类的方法外面,也可以定义在外部类的方法体中,如下所示: + ```java + public class Outer { + int out_x = 0; + public void method() + { + Inner1 inner1 = new Inner1(); + public class Inner2 //在方法体内部定义的内部类 + { + public method() + { + out_x = 3; + } + } + Inner2 inner2 = new Inner2(); + } + public class Inner1 //在方法体外面定义的内部类 + { + } + } + ``` + 在方法体外面定义的内部类的访问类型可以是public,protecte,默认的,private等4种类型,这就好像类中定义的成员变量有4种访问类型一样, + 它们决定这个内部类的定义对其他类是否可见;对于这种情况,我们也可以在外面创建内部类的实例对象,创建内部类的实例对象时, + 一定要先创建外部类的实例对象,然后用这个外部类的实例对象去创建内部类的实例对象,代码如下: + ```java + Outer outer = new Outer(); + Outer.Inner1 inner1 = outer.new Innner1(); + ``` + 在方法内部定义的内部类前面不能有访问类型修饰符,就好像方法中定义的局部变量一样,但这种内部类的前面可以使用final或abstract修饰符。 + 这种内部类对其他类是不可见的其他类无法引用这种内部类,但是这种内部类创建的实例对象可以传递给其他类访问。这种内部类必须是先定义, + 后使用,即内部类的定义代码必须出现在使用该类之前,这与方法中的局部变量必须先定义后使用的道理也是一样的。这种内部类可以访问方法体中的局部变量, + 但是,该局部变量前必须加final修饰符。 +25. 内部类可以引用它的包含类的成员吗?有没有什么限制? + 完全可以。如果不是静态内部类,那没有什么限制! + 如果你把静态嵌套类当作内部类的一种特例,那在这种情况下不可以访问外部类的普通成员变量,而只能访问外部类中的静态成员,例如,下面的代码: + ```java + class Outer { + static int x; + static class Inner + { + void test() + { + syso(x); + } + } + } + ``` + 答题时,也要能察言观色,揣摩提问者的心思,显然人家希望你说的是静态内部类不能访问外部类的成员,但你一上来就顶牛,这不好,要先顺着人家, + 让人家满意,然后再说特殊情况,让人家吃惊。 +26. Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可以implements(实现)interface(接口)? + 可以继承其他类或实现其他接口。不仅是可以,而是必须! +27. super.getClass()方法调用 + 下面程序的输出结果是多少? + ```java + import java.util.Date; + public class Test extends Date{ + public static void main(String[] args) { + new Test().test(); + } + public void test(){ + System.out.println(super.getClass().getName()); + } + } + ``` + 很奇怪,结果是Test + 这属于脑筋急转弯的题目,在一个qq群有个网友正好问过这个问题,我觉得挺有趣,就研究了一下,没想到今天还被你面到了,哈哈。 + 在test方法中,直接调用getClass().getName()方法,返回的是Test类名 + 由于getClass()在Object类中定义成了final,子类不能覆盖该方法,所以,在 + test方法中调用getClass().getName()方法,其实就是在调用从父类继承的getClass()方法,等效于调用super.getClass().getName()方法, + 所以,super.getClass().getName()方法返回的也应该是Test。如果想得到父类的名称,应该用如下代码: + `getClass().getSuperClass().getName();` + +28. jdk中哪些类是不能继承的? + 不能继承的是类是那些用final关键字修饰的类。一般比较基本的类型或防止扩展类无意间破坏原来方法的实现的类型都应该是final的, + 在jdk中System,String,StringBuffer等都是基本类型。 +29. String是最基本的数据类型吗? + 基本数据类型包括byte、int、char、long、float、double、boolean和short。 + java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用StringBuffer类 + +30. String s = "Hello";s = s + " world!";这两行代码执行后,原始的String对象中的内容到底变了没有? + 没有。因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s原先指向一个String对象,内容是 "Hello", + 然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个 String对象, + 内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。 + 通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的内存开销。 + 因为 String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用StringBuffer类,它允许修改, + 而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。 + 同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化, + 把它设置为初始值,应当这样做: + ```java + public class Demo { + private String s; + ... + public Demo { + s = "Initial Value"; + } + ... + } + ``` + 而非 + `s = new String("Initial Value");` + 后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。 + 也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。 + 上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java认为它们代表同一个String对象。而用关键字new调用构造器,总是会创建一个新的对象, + 无论内容是否相同。至于为什么要把String类设计成不可变类,是它的用途决定的。其实不只String,很多Java标准类库中的类都是不可变的。 + 在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的, + 所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以Java标准类库还提供了一个可变版本, + 即 StringBuffer。 + +31. String s = new String("xyz");创建了几个String Object? 二者之间有什么区别? + 两个或一个,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那一个。 + New String每写一遍,就创建一个新的对象,它一句那个常量”xyz”对象的内容来创建出一个新String对象。如果以前就用过’xyz’, + 这句代表就不会创建”xyz”自己了,直接从缓冲区拿。 +32. String 和StringBuffer的区别 + JAVA平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。String类表示内容不可改变的字符串。 + 而StringBuffer类表示内容可以被修改的字符串。当你知道字符数据要改变的时候你就可以使用StringBuffer。典型地,你可以使用StringBuffers来动态构造字符数据。 + 另外,String实现了equals方法,new String(“abc”).equals(new String(“abc”)的结果为true,而StringBuffer没有实现equals方法, + 所以,new StringBuffer(“abc”).equals(new StringBuffer(“abc”)的结果为false。 + 接着要举一个具体的例子来说明,我们要把1到100的所有数字拼起来,组成一个串。 + ```java + StringBuffer sbf = new StringBuffer(); + for(int i=0;i<100;i++) { + sbf.append(i); + } + ``` + 上面的代码效率很高,因为只创建了一个StringBuffer对象,而下面的代码效率很低,因为创建了101个对象。 + ```java + String str = new String(); + for(int i=0;i<100;i++) { + str = str + i; + } + ``` + 在讲两者区别时,应把循环的次数搞成10000,然后用endTime-beginTime来比较两者执行的时间差异,最后还要讲讲StringBuilder与StringBuffer的区别。 + String覆盖了equals方法和hashCode方法,而StringBuffer没有覆盖equals方法和hashCode方法,所以,将StringBuffer对象存储进Java集合类中时会出现问题。 + +33. StringBuffer与StringBuilder的区别 + StringBuffer和StringBuilder类都表示内容可以被修改的字符串,StringBuilder是线程不安全的,运行效率高,如果一个字符串变量是在方法里面定义, + 这种情况只可能有一个线程访问它,不存在不安全的因素了,则用StringBuilder。如果要在类里面定义成员变量,并且这个类的实例对象会在多线程环境下使用, + 那么最好用StringBuffer。 + +34. 如何把一段逗号分割的字符串转换成一个数组? + 如果不查jdk api,我很难写出来!我可以说说我的思路: + - 用正则表达式,代码大概为:String [] result = orgStr.split(“,”); + - 用 StingTokenizer ,代码为: + ```java + StringTokenizer tokener = StringTokenizer(orgStr,”,”); + String [] result = new String[tokener .countTokens()]; + Int i=0; + while(tokener.hasNext(){ + result[i++]=toker.nextToken(); + } + ``` +35. 数组有没有length()这个方法? String有没有length()这个方法? + 数组没有length()这个方法,有length的属性。String有length()这个方法。 + +36. 下面这条语句一共创建了多少个对象:String s="a"+"b"+"c"+"d"; + 答:对于如下代码: + String s1 = "a"; + String s2 = s1 + "b"; + String s3 = "a" + "b"; + System.out.println(s2 == "ab"); + System.out.println(s3 == "ab"); + 第一条语句打印的结果为false,第二条语句打印的结果为true,这说明javac编译可以对字符串常量直接相加的表达式进行优化, + 不必要等到运行期去进行加法运算处理,而是在编译时去掉其中的加号,直接将其编译成一个这些常量相连的结果。 + 题目中的第一行代码被编译器在编译时优化后,相当于直接定义了一个”abcd”的字符串,所以,上面的代码应该只创建了一个String对象。写如下两行代码, + ```java + String s = "a" + "b" + "c" + "d"; + System.out.println(s == "abcd"); + ``` + 最终打印的结果应该为true。 + +37. try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后? + 也许你的答案是在return之前,但往更细地说,我的答案是在return中间执行,请看下面程序代码的运行结果: + ```java + public class Test { + /** + * @param args add by zxx ,Dec 9, 2008 + */ + public static void main(String[] args) { + // TODO Auto-generated method stub + System.out.println(new Test().test());; + } + static int test() { + int x = 1; + try + { + return x; + } + finally + { + ++x; + } + } + } + ``` + ---------执行结果 --------- + 1 + + 运行结果是1,为什么呢?主函数调用子函数并得到结果的过程,好比主函数准备一个空罐子,当子函数要返回结果时,先把结果放在罐子里, + 然后再将程序逻辑返回到主函数。所谓返回,就是子函数说,我不运行了,你主函数继续运行吧,这没什么结果可言,结果是在说这话之前放进罐子里的。 +38. 下面的程序代码输出的结果是多少? + ```java + public class smallT { + public static void main(String args[]) { + smallT t = new smallT(); + int b = t.get(); + System.out.println(b); + } + public int get() { + try + { + return 1 ; + } + finally + { + return 2 ; + } + } + } + 返回的结果是2。 + 我可以通过下面一个例子程序来帮助我解释这个答案,从下面例子的运行结果中可以发现,try中的return语句调用的函数先于finally中调用的函数执行, + 也就是说return语句先执行,finally语句后执行,所以,返回的结果是2。Return并不是让函数马上返回,而是return语句执行后,将把返回结果放置进函数栈中, + 此时函数并不是马上返回,它要执行finally语句后才真正开始返回。 + 在讲解答案时可以用下面的程序来帮助分析: + ```java + public class Test { + /** + * @param args add by zxx ,Dec 9, 2008 + */ + public static void main(String[] args) { + // TODO Auto-generated method stub + System.out.println(new Test().test());; + } + int test() { + try + { + return func1(); + } + finally + { + return func2(); + } + } + int func1() { + System.out.println("func1"); + return 1; + } + int func2() { + System.out.println("func2"); + return 2; + } + } + -----------执行结果----------------- + func1 + func2 + 2 + ``` + 结论:finally中的代码比return 和break语句后执行 +39. final, finally, finalize的区别。 + final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 + 内部类要访问局部变量,局部变量必须定义成final类型. + finally是异常处理语句结构的一部分,表示总是执行。 + finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。 + JVM不保证此方法总被调用 + +40. 运行时异常与一般异常有何异同? + 异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。 + java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。 +41. error和exception有什么区别? + error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。 + exception 表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。 + +42. java中有几种方法可以实现一个线程?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用? + java5以前,有如下两种: + 第一种: + ```java + new Thread(){}.start();这表示调用Thread子类对象的run方法,new Thread(){}表示一个Thread的匿名子类的实例对象,子类加上run方法后的代码如下: + new Thread(){ + public void run(){ + } + }.start(); + ``` + 第二种: + ```java + new Thread(new Runnable(){}).start();这表示调用Thread对象接受的Runnable对象的run方法,new Runnable(){}表示一个Runnable的匿名子类的实例对象,runnable的子类加上run方法后的代码如下: + new Thread(new Runnable(){ + public void run(){ + } + } + ).start(); + ``` + 从java5开始,还有如下一些线程池创建多线程的方式: + ```java + ExecutorService pool = Executors.newFixedThreadPool(3) + for(int i=0;i<10;i++) { + pool.execute(new Runable(){public void run(){}}); + } + Executors.newCachedThreadPool().execute(new Runable(){public void run(){}}); + Executors.newSingleThreadExecutor().execute(new Runable(){public void run(){}}); + ``` + + 两种实现方法,分别是继承Thread类与实现Runnable接口 + 用synchronized关键字修饰同步方法 + 反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。 + 结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。 + 此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源, + 就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。 + 若标志指出线程应当恢复,则用一个notify()重新启动线程。 + +43. 启动一个线程是用run()还是start()? + 启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。 + +44. 简述synchronized和java.util.concurrent.locks.Lock的异同 ? + 主要相同点:Lock能完成synchronized所实现的所有功能 + 主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放, + 并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。 + +45. 设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 + 以下程序使用内部类实现线程,对j增减的时候没有考虑顺序问题。 + ```java + public class ThreadTest1 + { + private int j; + public static void main(String args[]){ + ThreadTest1 tt=new ThreadTest1(); + Inc inc=tt.new Inc(); + Dec dec=tt.new Dec(); + for(int i=0;i<2;i++){ + Thread t=new Thread(inc); + t.start(); + t=new Thread(dec); + t.start(); + } + } + private synchronized void inc(){ + j++; + System.out.println(Thread.currentThread().getName()+"-inc:"+j); + } + private synchronized void dec(){ + j--; + System.out.println(Thread.currentThread().getName()+"-dec:"+j); + } + class Inc implements Runnable{ + public void run(){ + for(int i=0;i<100;i++){ + inc(); + } + } + } + class Dec implements Runnable{ + public void run(){ + for(int i=0;i<100;i++){ + dec(); + } + } + } + } + ----------随手再写的一个------------- + class A { + JManger j =new JManager(); + main() { + new A().call(); + } + void call { + for(int i=0;i<2;i++) { + new Thread( + new Runnable(){ public void run(){while(true){j.accumulate()}}} + ).start(); + new Thread(new Runnable(){ public void run(){while(true){j.sub()}}}).start(); + } + } + } + class JManager { + private j = 0; + public synchronized void subtract() { + j-- + } + public synchronized void accumulate() { + j++; + } + } + ``` + +46. 子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。 + ```java + public class ThreadTest { + /** + * @param args + */ + public static void main(String[] args) { + // TODO Auto-generated method stub + new ThreadTest().init(); + } + public void init() { + final Business business = new Business(); + new Thread( + new Runnable() { + public void run() { + for(int i=0;i<50;i++) { + business.SubThread(i); + } + } + } + ).start(); + for(int i=0;i<50;i++) { + business.MainThread(i); + } + } + private class Business { + boolean bShouldSub = true;//这里相当于定义了控制该谁执行的一个信号灯 + public synchronized void MainThread(int i) { + if(bShouldSub) + try { + this.wait(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + for(int j=0;j<5;j++) { + System.out.println(Thread.currentThread().getName() + ":i=" + i +",j=" + j); + } + bShouldSub = true; + this.notify(); + } + public synchronized void SubThread(int i) { + if(!bShouldSub) + try { + this.wait(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + for(int j=0;j<10;j++) { + System.out.println(Thread.currentThread().getName() + ":i=" + i +",j=" + j); + } + bShouldSub = false; + this.notify(); + } + } + } + 备注:不可能一上来就写出上面的完整代码,最初写出来的代码如下,问题在于两个线程的代码要参照同一个变量,即这两个线程的代码要共享数据, + 所以,把这两个线程的执行代码搬到同一个类中去: + ```java + package com.huawei.interview.lym; + public class ThreadTest { + private static boolean bShouldMain = false; + public static void main(String[] args) { + // TODO Auto-generated method stub + /*new Thread(){ + public void run() { + for(int i=0;i<50;i++) { + for(int j=0;j<10;j++) { + System.out.println("i=" + i + ",j=" + j); + } + } + } + }.start();*/ + //final String str = new String(""); + new Thread( + new Runnable() { + public void run() { + for(int i=0;i<50;i++) { + synchronized (ThreadTest.class) { + if(bShouldMain) { + try { + ThreadTest.class.wait();} + catch (InterruptedException e) { + e.printStackTrace(); + } + } + for(int j=0;j<10;j++) { + System.out.println( + Thread.currentThread().getName() + + "i=" + i + ",j=" + j); + } + bShouldMain = true; + ThreadTest.class.notify(); + } + } + } + } + ).start(); + + for(int i=0;i<50;i++) { + synchronized (ThreadTest.class) { + if(!bShouldMain) { + try { + ThreadTest.class.wait();} + catch (InterruptedException e) { + e.printStackTrace(); + } + } + for(int j=0;j<5;j++) { + System.out.println( + Thread.currentThread().getName() + + "i=" + i + ",j=" + j); + } + bShouldMain = false; + ThreadTest.class.notify(); + } + } + } + ``` + 下面使用jdk5中的并发库来实现的: + ```java + import java.util.concurrent.Executors; + import java.util.concurrent.ExecutorService; + import java.util.concurrent.locks.Lock; + import java.util.concurrent.locks.ReentrantLock; + import java.util.concurrent.locks.Condition; + + public class ThreadTest { + private static Lock lock = new ReentrantLock(); + private static Condition subThreadCondition = lock.newCondition(); + private static boolean bBhouldSubThread = false; + public static void main(String [] args) { + ExecutorService threadPool = Executors.newFixedThreadPool(3); + threadPool.execute(new Runnable(){ + public void run() { + for(int i=0;i<50;i++) { + lock.lock(); + try { + if(!bBhouldSubThread) + subThreadCondition.await(); + for(int j=0;j<10;j++) { + System.out.println(Thread.currentThread().getName() + ",j=" + j); + } + bBhouldSubThread = false; + subThreadCondition.signal(); + }catch(Exception e) { + } + finally { + lock.unlock(); + } + } + } + + }); + threadPool.shutdown(); + for(int i=0;i<50;i++) { + lock.lock(); + try { + if(bBhouldSubThread) + subThreadCondition.await(); + for(int j=0;j<10;j++) { + System.out.println(Thread.currentThread().getName() + ",j=" + j); + } + bBhouldSubThread = true; + subThreadCondition.signal(); + } catch(Exception e) { + } + finally { + lock.unlock(); + } + } + } + } + ``` + +47. ArrayList和Vector的区别 + 这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组, + 我们以后可以按位置索引号取出某个元素,,并且其中的数据是允许重复的,这是HashSet之类的集合的最大不同处,HashSet之类的集合不可以按索引号去检索其中的元素, + 也不允许有重复的元素(本来题目问的与hashset没有任何关系,但为了说清楚ArrayList与Vector的功能,我们使用对比方式,更有利于说明问题)。 + 接着才说ArrayList与Vector的区别,这主要包括两个方面: + - 同步性: + Vector是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList是线程序不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合, + 那最好是使用ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们自己再去考虑和编写线程安全的代码。 + 备注:对于Vector&ArrayList、Hashtable&HashMap,要记住线程安全的问题,记住Vector与Hashtable是旧的,是java一诞生就提供了的,它们是线程安全的, + ArrayList与HashMap是java2时才提供的,它们是线程不安全的。所以,我们讲课时先讲老的。 + - 数据增长: + ArrayList与Vector都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList与Vector的存储空间,每次要增加存储空间时, + 不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector默认增长为原来两倍, + 而ArrayList的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍)。ArrayList与Vector都可以设置初始的空间大小, + Vector还可以设置增长的空间大小,而ArrayList没有提供设置增长空间的方法。 + 总结:即Vector增长原来的一倍,ArrayList增加原来的0.5倍。 + +48. HashMap和Hashtable的区别 + HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全, + 在只有一个线程访问的情况下,效率要高于Hashtable。 + HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。 + HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。 + Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。 + 最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。 + Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。 + 就HashMap与HashTable主要从三方面来说。 + - 历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现 + - 同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的 + - 值:只有HashMap可以让你将空值作为一个表的条目的key或value + +49. List 和 Map 区别? + 一个是存储单列数据的集合,另一个是存储键和值这样的双列数据的集合,List中存储的数据是有顺序,并且允许重复;Map中存储的数据是没有顺序的, + 其键是不能重复的,它的值是可以有重复的。 + +50. List, Set, Map是否继承自Collection接口? + List,Set是,Map不是 + +51. List、Map、Set三个接口,存取元素时,各有什么特点? + 这样的题属于随意发挥题:这样的题比较考水平,两个方面的水平:一是要真正明白这些内容,二是要有较强的总结和表述能力。如果你明白, + 但表述不清楚,在别人那里则等同于不明白。 + 首先,List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个功共同的父接口,叫Collection。Set里面不允许有重复的元素,所谓重复, + 即不能有两个相等(注意,不是仅仅是相同)的对象 ,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等, + 则B对象存储不进去,所以,Set集合的add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true, + 当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。Set取元素时,没法说取第几个,只能以Iterator接口取得所有的元素, + 再逐一遍历各个元素。List表示有先后顺序的集合, 注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obj e)方法时, + 每次加入的对象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用add(int index,Obj e)方法, + 就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List中,每调用一次add方法,这个对象就被插入进集合中一次, + 其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个索引指向了这个对象, + 如图x所示。List除了可以以Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以调用get(index i)来明确说明取第几个。 + Map与List和Set不同,它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key, + 这个重复的规则也是按equals比较相等。取则可以根据key获得相应的value,即get(Object key)返回值为key 所对应的value。另外,也可以获得所有的key的结合, + 还可以获得所有的value的结合,还可以获得key和value组合成的Map.Entry对象的集合。 + List 以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,内部排序。Map 保存key-value值,value可多值。 + HashSet按照hashcode值的某种运算方式进行存储,而不是直接按hashCode值的大小进行存储。 + 例如,"abc" ---> 78,"def" ---> 62,"xyz" ---> 65在hashSet中的存储顺序不是62,65,78,LinkedHashSet按插入的顺序存储, + 那被存储对象的hashcode方法还有什么作用呢?学员想想!hashset集合比较两个对象是否相等,首先看hashcode方法是否相等,然后看equals方法是否相等。 + new 两个Student插入到HashSet中,看HashSet的size,实现hashcode和equals方法后再看size。 + 同一个对象可以在Vector中加入多次。往集合里面加元素,相当于集合里用一根绳子连接到了目标对象。往HashSet中却加不了多次的。 + +52. 说出ArrayList,Vector, LinkedList的存储性能和特性 + ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素, + 但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差, + 而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。 + LinkedList也是线程不安全的,LinkedList提供了一些方法,使得LinkedList可以被当作堆栈和队列来使用。 + +53. 去掉一个Vector集合中重复的元素 + ```java + Vector newVector = new Vector(); + For (int i=0;i1 ){ + regex = "" + seperators[0] + "|" + seperators[1]; + }else{ + regex = "" + seperators[0]; + } + words = results.split(regex); + } + public String nextWord(){ + if(pos == words.length) + return null; + return words[pos++]; + } + } + ``` + +3. 编写一个程序,将d:\java目录下的所有.java文件复制到d:\jad目录下,并将原来文件的扩展名从.java改为.jad。 + listFiles方法接受一个FileFilter对象,这个FileFilter对象就是过虑的策略对象,不同的人提供不同的FileFilter实现,即提供了不同的过滤策略。 + ```java + import java.io.File; + import java.io.FileInputStream; + import java.io.FileOutputStream; + import java.io.FilenameFilter; + import java.io.IOException; + import java.io.InputStream; + import java.io.OutputStream; + public class Jad2Java { + public static void main(String[] args) throws Exception { + File srcDir = new File("java"); + if(!(srcDir.exists() && srcDir.isDirectory())) + throw new Exception("目录不存在"); + File[] files = srcDir.listFiles( + new FilenameFilter(){ + public boolean accept(File dir, String name) { + return name.endsWith(".java"); + } + } + ); + System.out.println(files.length); + File destDir = new File("jad"); + if(!destDir.exists()) destDir.mkdir(); + for(File f :files){ + FileInputStream fis = new FileInputStream(f); + String destFileName = f.getName().replaceAll("\\.java$", ".jad"); + FileOutputStream fos = new FileOutputStream(new File(destDir,destFileName)); + copy(fis,fos); + fis.close(); + fos.close(); + } + } + private static void copy(InputStream ips,OutputStream ops) throws Exception{ + int len = 0; + byte[] buf = new byte[1024]; + while((len = ips.read(buf)) != -1){ + ops.write(buf,0,len); + } + } + ``` + 由本题总结的思想及策略模式的解析: + 1. + ```java + class jad2java{ + 1. 得到某个目录下的所有的java文件集合 + 1.1 得到目录 File srcDir = new File("d:\\java"); + 1.2 得到目录下的所有java文件:File[] files = srcDir.listFiles(new MyFileFilter()); + 1.3 只想得到.java的文件: class MyFileFilter implememyts FileFilter{ + public boolean accept(File pathname){ + return pathname.getName().endsWith(".java") + } + } + 2.将每个文件复制到另外一个目录,并改扩展名 + 2.1 得到目标目录,如果目标目录不存在,则创建之 + 2.2 根据源文件名得到目标文件名,注意要用正则表达式,注意.的转义。 + 2.3 根据表示目录的File和目标文件名的字符串,得到表示目标文件的File。 + //要在硬盘中准确地创建出一个文件,需要知道文件名和文件的目录。 + 2.4 将源文件的流拷贝成目标文件流,拷贝方法独立成为一个方法,方法的参数采用抽象流的形式。 + //方法接受的参数类型尽量面向父类,越抽象越好,这样适应面更宽广。 + } + ``` + 分析listFiles方法内部的策略模式实现原理 + ```java + File[] listFiles(FileFilter filter){ + File[] files = listFiles(); + //Arraylist acceptedFilesList = new ArrayList(); + File[] acceptedFiles = new File[files.length]; + int pos = 0; + for(File file: files){ + boolean accepted = filter.accept(file); + if(accepted){ + //acceptedFilesList.add(file); + acceptedFiles[pos++] = file; + } + } + Arrays.copyOf(acceptedFiles,pos); + //return (File[])accpetedFilesList.toArray(); + } + ``` + +4. 编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串,但要保证汉字不被截取半个,如“我ABC”,4,应该截取“我AB”, + 输入“我ABC汉DEF”,6,应该输出“我ABC”,而不是“我ABC+汉的半个”。 + ```java + 首先要了解中文字符有多种编码及各种编码的特征。 + 假设n为要截取的字节数。 + public static void main(String[] args) throws Exception{ + String str = "我a爱中华abc我爱传智def'; + String str = "我ABC汉"; + int num = trimGBK(str.getBytes("GBK"),5); + System.out.println(str.substring(0,num) ); + } + public static int trimGBK(byte[] buf,int n){ + int num = 0; + boolean bChineseFirstHalf = false; + for(int i=0;i=’0’ && ch<=’9’) { + digitCount++ + } else if((ch>=’a’ && ch<=’z’) || (ch>=’A’ && ch<=’Z’)) { + engishCount++; + } else { + chineseCount++; + } + } + System.out.println(……………); + ``` + +6. 说明生活中遇到的二叉树,用java实现二叉树 + 这是组合设计模式。 + 我有很多个(假设10万个)数据要保存起来,以后还需要从保存的这些数据中检索是否存在某个数据,(我想说出二叉树的好处,该怎么说呢?那就是说别人的缺点), + 假如存在数组中,那么,碰巧要找的数字位于99999那个地方,那查找的速度将很慢,因为要从第1个依次往后取,取出来后进行比较。 + 平衡二叉树(构建平衡二叉树需要先排序,我们这里就不作考虑了)可以很好地解决这个问题,但二叉树的遍历(前序,中序,后序)效率要比数组低很多, + 代码如下: + ```java + package com.huawei.interview; + public class Node { + public int value; + public Node left; + public Node right; + public void store(int value) { + if(valuethis.value) { + if(right == null) { + right = new Node(); + right.value=value; + } else { + right.store(value); + } + } + } + public boolean find(int value) { + System.out.println("happen " + this.value); + if (value == this.value) { + return true; + } else if(value>this.value) { + if(right == null) return false; + return right.find(value); + } else { + if(left == null) return false; + return left.find(value); + } + } + public void preList() { + System.out.print(this.value + ","); + if(left!=null) left.preList(); + if(right!=null) right.preList(); + } + public void middleList() { + if(left!=null) left.preList(); + System.out.print(this.value + ","); + if(right!=null) right.preList(); + } + public void afterList() { + if(left!=null) left.preList(); + if(right!=null) right.preList(); + System.out.print(this.value + ","); + } + public static void main(String [] args) { + int [] data = new int[20]; + for(int i=0;i this.value) { + if(right != null) { + right.add(value); + } else { + Node node = new Node(value); + right = node; + } + } else { + if(left != null) { + left.add(value); + } else { + Node node = new Node(value); + left = node; + } + } + } + public boolean find(int value){ + if(value == this.value) { + return true; + } else if(value > this.value){ + if(right == null) return false; + else return right.find(value); + }else{ + if(left == null) return false; + else return left.find(value); + } + } + public void display(){ + System.out.println(value); + if(left != null) left.display(); + if(right != null) right.display(); + } + /*public Iterator iterator(){ + }*/ + public static void main(String[] args){ + int[] values = new int[8]; + for(int i=0;i<8;i++){ + int num = (int)(Math.random() * 15); + //System.out.println(num); + //if(Arrays.binarySearch(values, num)<0) + if(!contains(values,num)) + values[i] = num; + else + i--; + } + System.out.println(Arrays.toString(values)); + Node root = new Node(values[0]); + for(int i=1;iuser2.value) { + return 1; + }else { + return user1.name.compareTo(user2.name); + } + } + } + ); + Iterator iterator = results.keySet().iterator(); + while(iterator.hasNext()) { + String name = (String)iterator.next(); + Integer value = (Integer)results.get(name); + if(value > 1) { + sortedResults.add(new User(name,value)); + } + } + printResults(sortedResults); + } + private static void printResults(TreeSet sortedResults) { + Iterator iterator = sortedResults.iterator(); + while(iterator.hasNext()) { + User user = (User)iterator.next(); + System.out.println(user.name + ":" + user.value); + } + } + public static void dealLine(String line,Map map) { + if(!"".equals(line.trim())) { + String [] results = line.split(","); + if(results.length == 3) { + String name = results[1]; + Integer value = (Integer)map.get(name); + if(value == null) value = 0; + map.put(name,value + 1); + } + } + } + } + ``` +9. 递归算法题 + 一个整数,大于0,不用循环和本地变量,按照n,2n,4n,8n的顺序递增,当值大于5000时,把值按照指定顺序输出来。 + 例:n=1237 + 则输出为: + 1237, + 2474, + 4948, + 9896, + 9896, + 4948, + 2474, + 1237, + 提示:写程序时,先只写按递增方式的代码,写好递增的以后,再增加考虑递减部分。 + ```java + public static void doubleNum(int n) { + System.out.println(n); + if(n<=5000) + doubleNum(n*2); + System.out.println(n); + } + ``` + +10. 递归算法题 + 第1个人10,第2个比第1个人大2岁,依次递推,请用递归方式计算出第8个人多大? + ```java + package cn.itcast; + import java.util.Date; + public class A1 { + public static void main(String [] args) { + System.out.println(computeAge(8)); + } + public static int computeAge(int n) { + if(n==1) return 10; + return computeAge(n-1) + 2; + } + } + public static void toBinary(int n,StringBuffer result) { + if(n/2 != 0) + toBinary(n/2,result); + result.append(n%2); + } + ``` + +11. 排序都有哪几种方法?请列举。用JAVA实现一个快速排序。 + 本人只研究过冒泡排序、选择排序和快速排序,下面是快速排序的代码: + ```java + public class QuickSort { + /** + * 快速排序 + * @param strDate + * @param left + * @param right + */ + public void quickSort(String[] strDate,int left,int right){ + String middle,tempDate; + int i,j; + i=left; + j=right; + middle=strDate[(i+j)/2]; + do{ + while(strDate[i].compareTo(middle)<0&& i0&& j>left) + j--; //找出右边比中间值小的数 + if(i<=j){ //将左边大的数和右边小的数进行替换 + tempDate=strDate[i]; + strDate[i]=strDate[j]; + strDate[j]=tempDate; + i++; + j--; + } + }while(i<=j); //当两者交错6时停止 + if(ileft){ + quickSort(strDate,left,j); + } + } + /** + * @param args + */ + public static void main(String[] args){ + String[] strVoid=new String[]{"11","66","22","0","55","22","0","32"}; + QuickSort sort=new QuickSort(); + sort.quickSort(strVoid,0,strVoid.length-1); + for(int i=0;i(一千零一拾一元整)输出。 + ```java + // 去零的代码: + return sb.reverse().toString().replaceAll("零[拾佰仟]","零").replaceAll("零+万","万").replaceAll("零+元","元").replaceAll("零+","零"); + public class RenMingBi { + /** + * @param args add by zxx ,Nov 29, 2008 + */ + private static final char[] data = new char[]{ + '零','壹','贰','叁','肆','伍','陆','柒','捌','玖' + }; + private static final char[] units = new char[]{ + '元','拾','佰','仟','万','拾','佰','仟','亿' + }; + public static void main(String[] args) { + // TODO Auto-generated method stub + System.out.println( + convert(135689123)); + } + public static String convert(int money) { + StringBuffer sbf = new StringBuffer(); + int unit = 0; + while(money!=0) { + sbf.insert(0,units[unit++]); + int number = money%10; + sbf.insert(0, data[number]); + money /= 10; + } + return sbf.toString(); + } + } + ``` + +---- +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" "b/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" new file mode 100644 index 00000000..f90b23af --- /dev/null +++ "b/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" @@ -0,0 +1,179 @@ +# Java并发编程之原子性、可见性以及有序性 + +- 缓存导致的可见性问题 +- 线程切换带来的原子性问题 +- 编译优化带来的有序性问题 + + +## 原子性(Atomicity) + +众所周知,原子是构成物质的基本单位,所以原子代表着不可分。 +即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 +最简单的一个例子就是银行转账问题,赋值或者`return`。比如`a = 1;`和 `return a;`这样的操作都具有原子性 +原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作! +加锁可以保证复合语句的原子性,Java中提供了两个高级指令 `monitorenter`和 `monitorexit`,也就是对应的synchronized同步锁来保证原子性。 + +非原子性操作 +类似`a += b`这样的操作不具有原子性,在某些`JVM`中`a += b`可能要经过这样三个步骤: + +- 取出`a`和`b` +- 计算`a+b` +- 将计算结果写入内存 + 如果有两个线程`t1`,`t2`在进行这样的操作。`t1`在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是`t2`开始执行,`t2`执行完毕后`t1`又把没有完成的第三步做完。这个时候就出现了错误, + 相当于`t2`的计算结果被无视掉了。所以上面的片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 + 类似的,像`a++`这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。 + +## 可见性(Visibility) + +可见性指的是当一个线程修改了共享变量后,其他线程能够立即得知这个修改。 + +在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化, +当线程要处理该变量时,多个处理器会将变量从主内存复制一份分别存储在自己的存储器中,等到进行完操作后,再赋值回主存。 +(这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数); +同样在单核处理器中这样由于备份造成的问题同样存在!这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同 +的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作 +覆盖掉了,这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 + +volatile、synchronized、final都可以解决可见性问题。 + +## 有序性(Ordering) + +有序性:即程序执行的顺序按照代码的先后顺序执行。 + +有序性简单来说就是程序代码执行的顺序是否按照我们编写代码的顺序执行,一般来说,为了提高性能,编译器和处理器会对指令做重排序, 重排序分3类 + +- 编译器优化重排序,在不改变单线程程序语义的前提下,改变代码的执行顺序 +- 指令集并行的重排序,对于不存在数据依赖的指令,处理器可以改变语句对应指令的执行顺序来充分利用CPU资源 +- 内存系统的重排序,也就是前面说的CPU的内存乱序访问问题 + +也就是说,我们编写的源代码到最终执行的指令,会经过三种重排序。 + +比如编写时顺序如下的程序: + +```java +1. a = 5; +2. b = 20; +3. c = a + b; +``` + +编译器优化后执行的顺序可能变成: + +```java +1. b = 20; +2. a = 5; +3. c = a + b; +``` + +在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果 + +在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking): + +```java +public class Singleton{ + private static Singleton instance; + public static Singleton getInstance(){ + if (instance == null){ + synchronized(Singleton.class){ + if(instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +} +``` + +我们先看 instance=newSingleton() 的未被编译器优化的操作 + +- 指令 1:分配一块内存M; +- 指令 2:在内存M上初始化Singleton对象; +- 指令 3:然后M的地址赋值给instance变量。 + +编译器优化后的操作指令 + +- 指令 1:分配一块内存M; +- 指令 2:将M的地址赋值给instance变量; +- 指令 3:然后在内存M上初始化Singleton对象。 + +现在有A,B两个线程,我们假设线程A先执行getInstance()方法,当执行编译器优化后的操作指令2时(此时候未完成对象的初始化), +这时候发生了线程切换,那么线程B进入,刚好执行到第一次判断instance==nul会发现instance不等于null了,所以直接返回instance, +而此时的instance是没有初始化过的。 + +Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义, +而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块 +只能串行地进入。 + +## **先行发生原则:** + +如果Java内存模型中所有的有序性都只靠volatile和synchronized来完成,那么有一些操作将会变的很啰嗦,但是我们在编写Java并发代码的 +时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(Happen-Before)的原则。这个原则非常重要,它是判断数据是否存在竞争, +线程是否安全的主要依赖。 + +先行发生原则是指Java内存模型中定义的两项操作之间的依序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能 +被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。下面是Java内存模型下一些”天然的“先行发生关系, +这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话, +它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。 + +- 程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。 +- 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。 +- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。 +- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。 +- 线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。 +- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。 +- 对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。 +- 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。 + +一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生“呢?也是不成立的, +一个典型的例子就是指令重排序。所以时间上的先后顺序与先生发生原则之间基本没有什么关系,所以衡量并发安全问题一切必须以先行发生原则为准。 + +```java +int i = 0; +boolean flag = false; +i = 1; //语句1 +flag = true; //语句2 +``` +上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的, +那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? +不一定,为什么呢?这里可能会发生指令重排序`(Instruction Reorder)`。 +下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序 +同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果 +并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会 +和代码顺序执行结果相同,那么它靠什么保证的呢? +再看下面一个例子: + +```java +int a = 10; //语句1 +int r = 2; //语句2 +a = a + 3; //语句3 +r = a*a; //语句4 +``` +这段代码有4个语句,那么可能的一个执行顺序是: +语句2->语句1->语句3->语句4 +那么可能不可能是这个执行顺序呢?语句2->语句1->语句4->语句3,这是不可能的,因为处理器在进行重排序时是会考虑指令之间的数据依赖性, +如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。 + +虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子: +```java +//线程1: +context = loadContext(); //语句1 +inited = true; //语句2 + +//线程2: +while(!inited ){ + sleep() +} +doSomethingwithconfig(context); +``` +上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为 +初始化工作已经完成, 那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化, +就会导致程序出错。 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执 +行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/MD5\345\212\240\345\257\206.md" "b/JavaKnowledge/MD5\345\212\240\345\257\206.md" similarity index 88% rename from "Java\345\237\272\347\241\200/MD5\345\212\240\345\257\206.md" rename to "JavaKnowledge/MD5\345\212\240\345\257\206.md" index 90820a5c..62a8dd01 100644 --- "a/Java\345\237\272\347\241\200/MD5\345\212\240\345\257\206.md" +++ "b/JavaKnowledge/MD5\345\212\240\345\257\206.md" @@ -1,40 +1,41 @@ -MD5加密 -=== - -`MD5`是一种不可逆的加密算法只能将原文加密,不能讲密文再还原去,原来把加密后将这个数组通过`Base64`给变成字符串, -这样是不严格的业界标准的做法是对其加密之后用每个字节`&15`然后就能得到一个`int`型的值,再将这个`int`型的值变成16进制的字符串.虽然MD5不可逆, -但是网上出现了将常用的数字用`md5`加密之后通过数据库查询,所以`MD5`简单的情况下仍然可以查出来,一般可以对其多加密几次或者`&15`之后再和别的数运算等, -这称之为*加盐*. - -```java -public class MD5Utils { - /** - * md5加密的工具方法 - */ - public static String encode(String password){ - try { - MessageDigest digest = MessageDigest.getInstance("md5"); - byte[] result = digest.digest(password.getBytes()); - StringBuilder sb = new StringBuilder();//有的数很小还不到10所以得到16进制的字符串有一个 - //的情况,这里对于小于10的值前面加上0 - //16进制的方式 把结果集byte数组 打印出来 - for(byte b :result){ - int number = (b&0xff);//加盐. - String str =Integer.toHexString(number); - if(str.length()==1){ - sb.append("0"); - } - sb.append(str); - } - return sb.toString(); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - return ""; - } - } -} -``` - ----- -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +MD5加密 +======= + +`MD5`是一种不可逆的加密算法只能将原文加密,不能将密文再还原回去,原来把加密后将这个数组通过`Base64`给变成字符串, +这样是不严格的业界标准的做法是对其加密之后用每个字节`&15`然后就能得到一个`int`型的值,再将这个`int`型的值变成16进制的字符串.虽然MD5不可逆, +但是网上出现了将常用的数字用`md5`加密之后通过数据库查询,所以`MD5`简单的情况下仍然可以查出来,一般可以对其多加密几次或者`&15`之后再和别的数运算等, +这称之为*加盐*. + +```java +public class MD5Utils { + /** + * md5加密的工具方法 + */ + public static String encode(String password){ + try { + MessageDigest digest = MessageDigest.getInstance("md5"); + byte[] result = digest.digest(password.getBytes()); + StringBuilder sb = new StringBuilder();//有的数很小还不到10所以得到16进制的字符串有一个 + //的情况,这里对于小于10的值前面加上0 + //16进制的方式 把结果集byte数组 打印出来 + for(byte b :result){ + int number = (b&0xff);//加盐. + String str =Integer.toHexString(number); + if(str.length()==1){ + sb.append("0"); + } + sb.append(str); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return ""; + } + } +} +``` + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" "b/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" similarity index 75% rename from "Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" rename to "JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" index d08971cf..02a3ad83 100644 --- "a/Java\345\237\272\347\241\200/MVC\344\270\216MVP\345\217\212MVVM.md" +++ "b/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" @@ -15,15 +15,17 @@ MVC 分层同时也简化了分组开发。不同的开发人员可同时开发 - 优点 - 耦合性低 - - 重用性高 - - 可维护性高 - - 有利软件工程化管理 - + - 重用性高 + - 可维护性高 + - 有利软件工程化管理 + - 缺点 - 没有明确的定义 - 视图与控制器间的过于紧密的连接 - 增加系统结构和实现的复杂性 +![image](https://github.com/CharonChui/Pictures/blob/master/mvc_model.png?raw=true) + MVP --- @@ -32,10 +34,9 @@ MVP 所有的交互都发生在`Presenter`内部,而在`MVC`中`View`会直接从`Model`中读取数据而不是通过`Controller`。 在`MVC`里,`View`是可以直接访问`Model`的!从而,`View`里会包含`Model`信息,不可避免的还要包括一些业务逻辑。 在`MVC`模型里,更关注的`Model`的不变,而同时有多个对`Model`的不同显示及`View`。所以,在`MVC`模型里,`Model`不依赖于`View`,但是`View`是依赖于`Model`的。 -不仅如此,因为有一些业务逻辑在`View`里实现了,导致要更改`View`也是比较困难的,至少那些业务逻辑是无法重用的。 +不仅如此,因为有一些业务逻辑在`View`里实现了,导致要更改`View`也是比较困难的,至少那些业务逻辑是无法重用的。 -![image](https://github.com/CharonChui/Pictures/blob/master/MVP.jpg?raw=true) -![image](https://github.com/CharonChui/Pictures/blob/master/MVC.jpg?raw=true) +![image](https://github.com/CharonChui/Pictures/blob/master/is-activity-god-the-mvp-architecture-10-638.jpg?raw=true) 在`MVP`里,`Presenter`完全把`Model`和`View`进行了分离,主要的程序逻辑在`Presenter`里实现。而且`Presenter`与具体的`View`是没有直接关联的, 而是通过定义好的接口进行交互,从而使得在变更`View`时候可以保持`Presenter`的不变,即重用! @@ -55,14 +56,24 @@ MVP - 缺点 由于对视图的渲染放在了`Presenter`中,所以视图和`Presenter`的交互会过于频繁。还有一点需要明白,如果`Presenter`过多地渲染了视图, - 往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么`Presenter`也需要变更了。 - 比如说,原本用来呈现`Html`的`Presenter`现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。 + 往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么`Presenter`也需要变更了。 + 比如说,原本用来呈现`Html`的`Presenter`现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。 MVVM --- MVVM是Model-View-ViewModel的简写。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/MVVM.png) + +MVVM模式将Presener改名为View Model,基本上与MVP模式完全一致,同样是以VM为核心,但是不同于MVP,MVVM采用了数据双向绑定的方案,替代了繁琐复杂的DOM操作。 +该模型中,View与VM保持同步,View绑定到VM的属性上,如果VM数据发生变化,通过数据绑定的方式,View会自动更新视图;VM同样也暴露出Model中的数据。 + +看起来MVVM很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图 +属性的修改导致。如果项目中打算用MVVM的话可以考虑使用官方的架构组件ViewModel、LiveData、DataBinding去实现MVVM + + + - 优点 `MVVM`模式和`MVC`模式一样,主要目的是分离视图`View`和模型`Model` - 低耦合。 @@ -74,4 +85,4 @@ MVVM是Model-View-ViewModel的简写。 - 邮箱 :charon.chui@gmail.com - Good Luck! - + diff --git "a/Java\345\237\272\347\241\200/RMB\345\244\247\345\260\217\345\206\231\350\275\254\346\215\242.md" "b/JavaKnowledge/RMB\345\244\247\345\260\217\345\206\231\350\275\254\346\215\242.md" similarity index 100% rename from "Java\345\237\272\347\241\200/RMB\345\244\247\345\260\217\345\206\231\350\275\254\346\215\242.md" rename to "JavaKnowledge/RMB\345\244\247\345\260\217\345\206\231\350\275\254\346\215\242.md" diff --git "a/JavaKnowledge/Top-K\351\227\256\351\242\230.md" "b/JavaKnowledge/Top-K\351\227\256\351\242\230.md" new file mode 100644 index 00000000..d716c354 --- /dev/null +++ "b/JavaKnowledge/Top-K\351\227\256\351\242\230.md" @@ -0,0 +1,410 @@ +Top-K问题 +=== + +`Top-K`问题在数据分析中非常普遍的一个问题(在面试中也经常被问到),比如: + +> 从1亿个数字中,找出其中最大的10000个数。 + +在一大堆数中求其前`k`大或前`k`小的问题,简称`Top-K`问题。而目前解决`Top-K`问题最有效的算法即是`BFPRT`算法,其又称为中位数的中位数算法, +该算法由`Blum`、`Floyd`、`Pratt`、`Rivest`、`Tarjan`提出,最坏时间复杂度为`O(n)`。 + +这个问题总共有几种解决方式: + +- 最容易的方法就是将数据全部排序 + + 在首次接触`TOP-K`问题时,我们的第一反应就是可以先对所有数据进行一次排序,然后取其前`k`即可,但是这么做有两个问题: + - 快速排序的平均复杂度为O(nlogn),但最坏时间复杂度为O(n^2),不能始终保证较好的复杂度。 + - 我们只需要前`k`大的,而对其余不需要的数也进行了排序,浪费了大量排序时间。 + + 而且在32位的机器上,每个`float`类型占4个字节,1亿个浮点数就要占用`400MB`的存储空间,所以对于内存的要求很高,而且不能一次将全部数据 + 读入内存进行排序。 + +- 局部淘汰法 + + 该方法与排序方法类似,用一个容器保存前`10000`个数,然后将剩余的所有数字逐一与容器内的最小数字相比,如果所有后续的元素都比容器内的`10000`个数还小, + 那么容器内这个`10000`个数就是最大`10000`个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这`1` + 亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为`O(n+m^2)`,其中`m`为容器的大小,即`10000`。 + +- 分治法 + + 将`1`亿个数据分成`100`份,每份`100`万个数据,找到每份数据中最大的`10000`个,最后在剩下的`100*10000`个数据里面找出最大的`10000`个。 + 如果`100`万数据选择足够理想,那么可以过滤掉`1`亿数据里面`99%`的数据。`100`万个数据里面查找最大的`10000`个数据的方法如下: + 用快速排序的方法,将数据分为`2`堆,如果大的那堆个数`N`大于`10000`个,继续对大堆快速排序一次分成`2`堆,如果大的那堆个数`N`大于`10000`个, + 继续对大堆快速排序一次分成`2`堆,如果大堆个数`N`小于`10000`个,就在小的那堆里面快速排序一次,找第`10000-n`大的数字;递归以上过程,就可 + 以找到第`10000`大的数。一共需要101次这样的比较。 + +- `Hash`法 + + 如果这`1`亿个数里面有很多重复的数,先通过`Hash`法,把这`1`亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间, + 然后通过分治法或最小堆法查找最大的`10000`个数。 + +- 最小堆 + + 首先读入前`10000`个数来创建大小为`10000`的最小堆,建堆的时间复杂度为`O(mlogm)`(`m`为数组的大小即为`10000`),然后遍历后续的数字, + 并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至`1`亿 + 个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有`10000`个数字。该算法的时间复杂度为`O(nmlogm)`,空间复杂度是10000(常数)。 + + +解决`Top K`问题有两种思路: + +- 最直观:小顶堆`(大顶堆 -> 最小100个数)` +- 较高效:`Quick Select`算法. + +堆排序也是一个比较好的选择,可以维护一个大小为`k`的堆,时间复杂度为`O(nlogk)`。 + +那是否还存在更有效的方法呢?受到[快速排序](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%85%AB%E7%A7%8D%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95.md)的启发, +通过修改快速排序中主元的选取方法可以降低快速排序在最坏情况下的时间复杂度(即`BFPRT`算法). + +并且我们的目的只是求出前`k`,故递归的规模变小,速度也随之提高。下面来简单回顾下快速排序的过程,以升序为例: + +- 选取主元(首元素,尾元素或一个随机元素) +- 以选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边 +- 分别对左边和右边进行递归,重复上述过程 + + + +堆 +--- + + +小顶堆`(min-heap)`有个重要的性质——每个结点的值均不大于其左右孩子结点的值,则堆顶元素即为整个堆的最小值。 +`JDK`中`PriorityQueue`实现了数据结构堆,通过指定`comparator`字段来表示小顶堆或大顶堆,默认为`null`,表示自然序`(natural ordering)`。 + +小顶堆解决`Top-K`问题的思路:小顶堆维护当前扫描到的最大100个数,其后每一次的扫描到的元素,若大于堆顶,则入堆,然后删除堆顶; +依此往复,直至扫描完所有元素。`Java`实现第`K`大整数代码如下: + +```java +public class TopK { + private PriorityQueue queue; + //堆的最大容量 + private int maxSize; + + public TopK(int maxSize) { + if (maxSize <= 0) { + throw new IllegalStateException(); + } + this.maxSize = maxSize; + this.queue = new PriorityQueue<>(maxSize, new Comparator() { + @Override + public int compare(E o1, E o2) { + // 最大堆用o2 - o1,最小堆用o1 - o2 + return (o1.compareTo(o2)); + } + }); + } + + public void add(E e) { + if (queue.size() < maxSize) { + queue.add(e); + } else { + E peek = queue.peek(); + if (e.compareTo(peek) > 0) { + queue.poll(); + queue.add(e); + } + } + } + + public List sortedList() { + List list = new ArrayList<>(queue); + Collections.sort(list); + return list; + } + + public static void main(String[] args) { + int[] array = {4, 5, 1, 6, 2, 7, 3, 8}; + TopK pq = new TopK(4); + for (int n : array) { + pq.add(n); + } + System.out.println(pq.sortedList()); + } +} +``` + +下面使用`Java`来实现 + +- 限定数据大小。 +- 若堆满,则插入过程中与堆顶元素比较,并做相应操作。 +- 每次删除堆顶元素后堆做一次调整,保证最小堆特性。 + +```java +public class Heap { + private int[] data; + + public Heap(int[] data) { + this.data = data; + buildHeap(); + } + + public void buildHeap() { + for (int i = data.length / 2 - 1; i >= 0; i--) { + heapity(i); + } + } + + public void heapity(int i) { + int left = getLeft(i); + int right = getRight(i); + int smallIndex = i; + if (left < data.length && data[left] < data[i]) + smallIndex = left; + if (right < data.length && data[right] < data[smallIndex]) + smallIndex = right; + if (smallIndex == i) + return; + swap(i, smallIndex); + heapity(smallIndex); + } + + public int getLeft(int i) { + return ((i + 1) << 1) - 1; + } + + public int getRight(int i) { + return (i + 1) << 1; + } + + public void swap(int i, int j) { + data[i] ^= data[j]; + data[j] ^= data[i]; + data[i] ^= data[j]; + } + + public int getMin() { + return data[0]; + } + + public void setMin(int i) { + data[0] = i; + heapity(0); + } +} + +public class TopK { + private static int[] topK(int[] data,int k){ + int topk[]=new int[k]; + for (int i = 0; i < k; i++) { + topk[i]=data[i]; + } + Heap heap=new Heap(topk); + for (int j = k; j < data.length; j++) { + int min=heap.getMin(); + if(data[j]>min) + heap.setMin(data[j]); + } + return topk; + } + public static void main(String[] args) { + int[] data = {33,86,59,46,84,76,1236,963}; + int[] topk=topK(data,3); + for (int i : topk) { + System.out.print(i+","); + } + } +} + +``` + + + + +`BFPRT`算法 +--- + +`BFPRT`算法步骤如下: + +本算法的最坏时间复杂度为`O(n)`,值得注意的是通过`BFPTR`算法将数组按第`K`小(大)的元素划分为两部分,而 +这高低两部分不一定是有序的,通常我们也不需要求出顺序,而只需要求出前`K`大的或者前`K`小的。 + +在`BFPTR`算法中,仅仅是改变了快速排序`Partion`中的`pivot`值的选取,在快速排序中,我们始终选择第一个元 +素或者最后一个元素作为`pivot`,而在`BFPTR`算法中,每次选择五分中位数的中位数作为`pivot`,这样做的目的 +就是使得划分比较合理,从而避免了最坏情况的发生。算法步骤如下: + +- 将输入数组的n个元素划分为`n/5`组,每组5个元素,且至多只有一个组由剩下的`n%5`个元素组成。 +- 寻找`n/5`个组中每一个组的中位数,首先对每组的元素进行插入排序,然后从排序过的序列中选出中位数。 +- 对于上面一步中找出的`n/5`个中位数,递归进行步骤`(1)`和`(2)`,直到只剩下一个数即为这`n/5`个元素的中位数,找到中位数后并找到对应的下标`p`。 +- 进行`Partion`划分过程,`Partion`划分中的`pivot`元素下标为`p`。 +- 进行高低区判断即可。 + +下面为代码实现,其所求为前`K`小的数: + +```java +public class BFPRT { + /** + * 返回前K小的数 + * + * @param arr + * @param k + * @return + */ + public static int[] getMinKNumsByBFPRT(int[] arr, int k) { + if (k < 1 || k > arr.length) { + return arr; + } + int minKth = getMinKthByBFPRT(arr, k); + int[] res = new int[k]; + int index = 0; + for (int i = 0; i != arr.length; i++) { + if (arr[i] < minKth) { + res[index++] = arr[i]; + } + } + for (; index != res.length; index++) { + res[index] = minKth; + } + return res; + } + + /** + * 返回数组中第K小的数 + * + * @param arr + * @param K + * @return + */ + public static int getMinKthByBFPRT(int[] arr, int K) { + int[] copyArr = copyArray(arr); + return select(copyArr, 0, copyArr.length - 1, K - 1); + } + + public static int[] copyArray(int[] arr) { + int[] res = new int[arr.length]; + for (int i = 0; i != res.length; i++) { + res[i] = arr[i]; + } + return res; + } + + /** + * 在数组上给一个 end - begin 的范围,在这个范围上,返回位于第 i 位置上的数 + * + * @param arr + * @param begin + * @param end + * @param i + * @return + */ + public static int select(int[] arr, int begin, int end, int i) { + if (begin == end) { + return arr[begin]; + } + int pivot = medianOfMedians(arr, begin, end); + int[] pivotRange = partition(arr, begin, end, pivot); + if (i >= pivotRange[0] && i <= pivotRange[1]) { + return arr[i]; + } else if (i < pivotRange[0]) { + return select(arr, begin, pivotRange[0] - 1, i); + } else if (i > pivotRange[1]) { + return select(arr, pivotRange[1] + 1, end, i); + } + + return 0; + } + + /** + * 求一个范围内的划分值 + * + * @param arr + * @param begin + * @param end + * @return + */ + public static int medianOfMedians(int[] arr, int begin, int end) { + int num = end - begin + 1; + int offset = num % 5 == 0 ? 0 : 1; + int[] mArr = new int[num / 5 + offset]; //所有中位数组成的数组 + for (int i = 0; i < mArr.length; i++) { + int beginI = begin + i * 5; + int endI = beginI + 4; + mArr[i] = getMedian(arr, beginI, Math.min(endI, end)); + } + return select(mArr, 0, mArr.length - 1, mArr.length / 2); + } + + /** + * @param arr + * @param begin + * @param end + * @param pivotValue 基准值 + * @return 返回等于区 最左边的位置 和 最右的位置 + */ + public static int[] partition(int[] arr, int begin, int end, int pivotValue) { + int small = begin - 1; + int big = end + 1; + int i = begin; + while (i < big) { + if (arr[i] < pivotValue) { + small++; + swap(arr, small, i); + i++; + } else if (arr[i] > pivotValue) { + big--; + swap(arr, i, big); + } else { + i++; + } + } + return new int[]{small + 1, big - 1}; + } + + /** + * 获取上中位数 + * eg 9 10 11 12 取10 + * eg 1 2 3 4 5 取3 + * + * @param arr + * @param begin + * @param end + * @return + */ + public static int getMedian(int[] arr, int begin, int end) { + insertionSort(arr, begin, end); + return arr[(end - begin) / 2 + begin]; + } + + /** + * 插入排序 + * + * @param arr + * @param begin + * @param end + */ + public static void insertionSort(int[] arr, int begin, int end) { + for (int i = begin + 1; i != end + 1; i++) { + for (int j = i; j != begin; j--) { + if (arr[j - 1] > arr[j]) { + swap(arr, j - 1, j); + } else { + break; + } + } + } + } + + public static void swap(int[] arr, int index1, int index2) { + int tmp = arr[index1]; + arr[index1] = arr[index2]; + arr[index2] = tmp; + } + + public static void printArray(int[] arr) { + System.out.print("前10小的数: "); + for (int i = 0; i != arr.length; i++) { + System.out.print(arr[i] + " "); + } + System.out.println(); + } + + public static void main(String[] args) { + int[] arr = {6, 9, 1, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9}; + printArray(getMinKNumsByBFPRT(arr, 10)); + System.out.println("第10小的数: " + getMinKthByBFPRT(arr, 10)); + } +} +``` + + +---- +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/JavaKnowledge/UML\347\261\273\345\233\276.pdf" "b/JavaKnowledge/UML\347\261\273\345\233\276.pdf" new file mode 100644 index 00000000..d7f9f7f7 Binary files /dev/null and "b/JavaKnowledge/UML\347\261\273\345\233\276.pdf" differ diff --git "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" new file mode 100644 index 00000000..3c2cffc6 --- /dev/null +++ "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" @@ -0,0 +1,144 @@ +Vim使用教程 +=== + +`Better, Stronger, Faster` + +首先`Vim`有两种模式: + +- `Normal` + 该模式下不能写入,修改要在该模式进行。在`Insert`模式中可以使用`ESC`键来返回到`Normal`模式。 +- `Insert` + 该模式下可以进行写入。在`Normal`模式下使用按`i`键进行`Insert`模式。 + +下面说一下`Normal`状态下的一些命令,所有的命令都要在`Normal`状态下执行: + +进入`Insert`模式 +--- + +- `i` 进入`insert`模式 +- `a(append)` 在光标后进行插入,直接进入`insert`模式 +- `o(open a line below)` 在当前行后插入一个新行,直接进入`insert`模式 +- `O` 大写的O是在光标所在的行下面插入一个新航,直接进入编译模式 +- `I` 从该行的最前面开始编辑 +- `A` 从该行的最后面开始编辑 +- `s` 删除光标后的字符,从光标当前位置插入 +- `S` 删除光标所在当前行,从行首插入 +- `cw` 替换从光标位置开始到该单词结束位置的所有字符,直接进入`Insert`模式 + +在VIM中,有相当一部分命令可以扩展为3部分: + +- 开头的部分是一个数字,代表重复次数; +- 中间的部分是命令; +- 最后的部分,代表命令的对象。 + +比如,命令3de中,3表示执行3次,d是删除命令,e表示从当前位置到单词的末尾。整条命令的意思就是,从当前位置向后,删除3个单词。类似的,命令3ce表示从当前位置向后,删除三个单词,然后进入编辑模式。 + +可以看到,命令组合的前两个部分比较简单,但第三个部分也就是命令对象,技巧就比较多了。所以接下来,我就与你详细介绍下到底有哪些命令对象可以使用。 + +其实,对命令对象并没有一个完整的分类。但我根据经验,将其总结为光标移动命令和文本对象两种。 + +第一种是光标移动命令。比如,$命令是移动光标到本行末尾,那么d$就表示删除到本行末尾;再比如,4}表示向下移动4个由空行隔开的段落,那么d4}就是删除这4个段落。 + +移动光标 +--- + +- `h` 左移 +- `j` 移到下一行 +- `k` 移到上一行 +- `l` 右移 +- `gg` 移动到文章的开头 +- `G` 移动到当前文章的最后。 + +- `$` $光标移动当前行尾 +- `0` 数字0光标移动当前行首 + +- `e` 向右移动一个单词 +- `w` 向右移动一个单词,与e的区别是w是把光标放到下一个单词的开头,而e是把光标放到这一个单词的结尾。 +- `b` 移动到单词开始位置 +- `:59` 移动到59行 +- `#l` 移动光标到该行第#个字的位置,如`5l` +- `ctrl+g` 列出当前光标所在行的行号等信息 +- `: #` 如输入: 15会跳到文章的第15行 +- `ctrl+b`:向上滚动一屏 +- `ctrl+f`:向下滚动一屏 +- `ctrl+u`:向上滚动半屏 +- `ctrl+d`:向下滚动半屏 + +删除文字 +--- + +- `x` 删除光标所在位置的一个字符 +- `#x` 删除光标所在位置后的#个字符,如`6x`就是删除后面的6个字符。 +- `X` 大写的X为删除光标所在位置前的一个字符 +- `#X` 删除光标所在位置前的#个字符 +- `dd` 删除当前行,并把删除的行存到剪贴板中 +- `#dd` 从光标所在行开始删除#行。如`5dd`就是删除5行 +- `v/ctrl+v`: 使用h、j、k、l移动选择内容,然后按d删除。其中v是非列模式,ctrl+v是列模式 + +复制粘贴 +--- + +- `yy` 拷贝当前行 +- `#yy` 拷贝当前所在行往下的#行文字 +- `yw` 复制当前光标所在位置到字尾处的位置 +- `#yw` 复制当前光标所在位置往后#个字 +- `y$` 拷贝光标至本行结束位置 +- `y` 拷贝选中部分,在`Normal`模式下按`v`会进入到可视化模式,这时候可以上下移动进行选中某一部分,然后按`y`就可以复制了。 + +- `p` 在光标所在的位置向下开辟一行,粘贴 +- `P` 在光标所在的位置向上开辟一行,粘贴 +- 剪切: 按dd或者ndd删除,将删除的行保存到剪贴板中,然后按p/P就可以粘贴了。 + + +替换 +--- + +- `r` 替换光标所在处的字符 +- `R` 替换光标所到之处的字符,直到按下`esc`键为止 +- `:%s/old/new/g` 用`new`替换文件中所有的`old` +- `:%s/old/new/gc`,同上,但是每次替换需要用户确认 +- `:s/old/new/g` 光标所在行的所有old替换为new +- `:s/old/new/` 光标所在行的第一个old替换为new + +撤销 +--- + +- `u` 撤销、回退 +- `ctrl + r` 恢复刚才的撤销操作 + +搜索 +--- + +- `/关键字` 先按`/`键,再输入您想寻找的字符,如果第一次找的关键字不是您想要的,可以一直按`n`会往后寻找到您要的关键字为止。 + +- `?关键字` 同上,只不过`?`是往上查找 + +缩进缩出 +--- + +- `>>` 当前行缩进 +- `#>>` 当前光标下n行缩进 +- `<<` 当前行缩出 +- `#<<` 当前光标下n行缩出 + +- `: set nu` 会在文件每一行前面显示行号 +- `:wq` 保存并退出 +- `:w` 保存 +- `:q!` 退出不保存 +- `:saveas ` 另存为 +- `:e filename` 打开文件 +- `:sav filename 保存为某文件名 +- ZZ: 命令模式使用大写ZZ直接保存并退出 + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/hashCode\344\270\216equals.md" "b/JavaKnowledge/hashCode\344\270\216equals.md" similarity index 67% rename from "Java\345\237\272\347\241\200/hashCode\344\270\216equals.md" rename to "JavaKnowledge/hashCode\344\270\216equals.md" index 75bf9067..c73130a0 100644 --- "a/Java\345\237\272\347\241\200/hashCode\344\270\216equals.md" +++ "b/JavaKnowledge/hashCode\344\270\216equals.md" @@ -1,11 +1,12 @@ hashCode与equals === -`HashSet`和`HashMap`一直都是`JDK`中最常用的两个类,`HashSet`要求不能存储相同的对象,`HashMap`要求不能存储相同的键。 那么`Java`运行时环境是如何判断`HashSet` -中相同对象、`HashMap`中相同键的呢?当存储了相同的东西之后`Java`运行时环境又将如何来维护呢? -在研究这个问题之前,首先说明一下`JDK`对`equals(Object obj)`和`hashcode()`这两个方法的定义和规范:在`Java`中任何一个对象都具备`equals(Object obj)` -和`hashcode()`这两个方法,因为他们是在`Object`类中定义的。`equals(Object obj)`方法用来判断两个对象是否“相同”,如果“相同”则返回`true`,否则返回`false`。 -`hashcode()`方法返回一个`int`数,在`Object`类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 +`HashSet`和`HashMap`一直都是`JDK`中最常用的两个类,`HashSet`要求不能存储相同的对象,`HashMap`要求不能存储相同的键。 那么`Java`运行时 +环境是如何判断`HashSet`中相同对象、`HashMap`中相同键的呢?当存储了相同的东西之后`Java`运行时环境又将如何来维护呢? +在研究这个问题之前,首先说明一下`JDK`对`equals(Object obj)`和`hashcode()`这两个方法的定义和规范: +在`Java`中任何一个对象都具备`equals(Object obj)`和`hashcode()`这两个方法,因为他们是在`Object`类中定义的: +- `equals(Object obj)`方法用来判断两个对象是否“相同”,如果“相同”则返回`true`,否则返回`false`。 +- `hashcode()`方法返回一个`int`数,在`Object`类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 接下来有两个个关于这两个方法的重要规范: - 若重写`equals(Object obj)`方法,有必要重写`hashcode()`方法,确保通过`equals(Object obj)`方法判断结果为`true`的两个对象具备相等的`hashcode()`返回值。 diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" new file mode 100644 index 00000000..35233217 --- /dev/null +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -0,0 +1,1246 @@ +# python3入门 + +## 变量 + +```python +message = "Hello World" +print(message) +``` + +常量: python中一版约定名字全为大写的就是常量。 + +## bool + + +bool类型只有True和False,用于真假判断。 + +python3中,bool是int的子类,True和False可以和数字相加。 + +True == 1、False == 0 会返回True + +is运算符用于比较两个对象的身份(即它们是否是同一个对象,是否在内存中占据相同的问题),而不是比较它们的值。 + +```python3 +print(True == 1) # True +print(False == 0) # True + +print(True is 1) # False +print(False is 0) # False + +``` + + +在python中,能够解释为假的值不只有False,还有: + +- None +- 0 +- 0.0 +- False +- 所有的空容器(空列表、空元组、空字典、空集合、空字符串) + +## 字符串 + +字符串就是一系列字符。在Python中,用引号括起来的都是字符串,其中的引号可以是单引号,也可以是双引号。例如: +```python +message = 'Hello World' +print(message) +message = "Hello World" +print(message) +``` + +### 大小写 + +修改单词中的大小写: + +- title()方法是以首字母大写的方式显示每个单词,即将每个单词的首字母都改为大写。 +- upper()方法将字符串改为全部大写。 +- lower()方法将字符串改为全部小写。 + +```python +message = 'hello world' +print(message.title()) +# 输出结果为Hello World +``` + +注意上面方法lower、upper、title等不会修改存储在变量message中的值。 + +### 拼接 + +合并: Python使用加号(+)来合并字符串。这种合并字符串的方法称为拼接。 +```python +first_name = "ada" +last_name = "lovelace" +full_name = first_name + " " + last_name +print(full_name) +# 输出: ada lovelace +``` + +### 空格 + +rstrip(): 去除字符串末尾空白。注意去除只是暂时的,等你再次访问该变量时,你会发现这个字符串仍然包含末尾空白。 +要永久删除这个字符串中的空白,必须将删除的结果存回到变量中: + + +first_name = "ada " +print(first_name) +first_name = first_name.rstrip() +print(first_name) + +- lstrip(): 去除字符串开头空白 +- strip(): 同时去除字符串两端的空白 + + +## 整数 + + +可对整数执行加(+)、减(-)、乘(`*`)、除(/)运算。 + +Python使用两个乘号表示乘方运算。 + + +## 浮点数 + +在Python中将带小数点的数字都称为浮点数。 +但是要注意的是,结果包含小数位数可能是不确定的,例如: +```python +>>> 0.2 + 0.1 +0.30000000000000004 +``` + +命令行执行python后执行exit()或quit()可退出。 + +## 类型转换 + +```python +age = 23 +message = "Happy" + age + "Birthday" +print(message) +``` + +执行时会报错: +```python +message = "Happy" + age + "Birthday" + ~~~~~~~~^~~~~ +TypeError: can only concatenate str (not "int") to str +``` + +类型错误,这个时候需要显式的指定希望Python将这个整数用作字符串。 +为此,可调用函数str(),它让Python将非字符串值表示为字符串: + +```python +age = 23 +message = "Happy" + str(age) + "Birthday" +print(message) +``` + +### 自动类型转换(隐式转换) + +对两种不同类型的数据进行运算,较小的数据类型(整数)就会转换为较大的数据类型(浮点数)以免数据丢失,计算结果为浮点型: + +```python3 +num1 = 2 +num2 = 3.0 + +print(num1 + num2) # 5.0 + + + +num1 = 9 +num2 = 1 +print(num1 / num2) #9.0 +``` +注意: 特别的,两个整形进行除法运算时结果也是浮点型。 + + + + + + + + + + + +## 注释 + +使用井号(#)标识注释 + + +## 列表 + +在Python中,用方括号[]来表示列表,并用逗号来分割其中的元素。 +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +print(bicycles) +print(bicycles[0]) +print(bicycles[-1]) +# 设置最后一个元素 +bicycles[-1] = 'honda' +# 在列表最后添加元素 +bicycles.append('ducati') +# 在某个位置添加元素 +bicycles.insert(0, 'a') +print(bicycles) +# 删除某个位置的元素 +del bicycles[0] +# 删除最后一个元素,并返回删除的值 +popValue = bicycles.pop() +print(popValue) +# 通过值删除元素,如果列表中有多个重复的元素,那该方法只会删除第一个指定的值 +bicycles.remove('trek') +# 排序 +bicycles.sort() +# 反序排序 +bicycles.sort(reverse=True) +``` +Python为访问最后一个列表元素提供了一种特殊的语法,通过将索引指定为-1,可让Python返回最后一个列表元素。 +这是为了方便在不知道列表长度的情况下访问最后的元素。 +这种约定也适用于其他负数索引,例如,索引-2返回倒数第二个列表元素,索引-3返回倒数第三个列表元素,以此类推。 + +### 临时排序 + +要保留列表元素原来的排列顺序,同时以特定的顺序呈现它们,可使用函数sorted()。 +函数sorted()让你能够按特定顺序显示列表元素,同时不影响它们在列表中的原始排列顺序。 + +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +bicycles.sort(reverse=True) +# ['trek', 'specialized', 'redline', 'cannondale'] +print(bicycles) +# ['cannondale', 'redline', 'specialized', 'trek'] +print(sorted(bicycles)) +# ['trek', 'specialized', 'redline', 'cannondale'] +print(bicycles) +``` + + +### 反转列表 + +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +bicycles.reverse() +print(bicycles) +# 列表长度 +size = len(bicycles) +print(size) +``` + +### 遍历 + +**Python根据缩进来判断代码行与前一个代码行的关系,在较长的Python程序中,你将看到缩进程度各不相同的代码块,这让你对程序的组织结构有大致的认识** + + +注意: + +- for in 后有个冒号`:`,for语句末尾的冒号告诉Python,下一行是循环的第一行。 +- 前面缩进的代码才是for循环中的部分 +- 没有缩进的是for循环之外的 + + +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +for bcy in bicycles: + print(bcy) + print(bcy.title()) +print('end') +``` + +#### 使用enumerate()可以同时获取列表的下标和元素。 + +```python +lst = [1, 2, 3] + +for i, value in enumerate(lst): + print(i, value) +``` + + +### 切片 + +你还可以处理列表的部分元素——Python称之为切片。 +要创建切片,可指定要使用的第一个元素和最后一个元素的索引。 +你可以生成列表的任何子集,例如,如果你要提取列表的第2~4个元素,可将起始索引指定为1,并将终止索引指定为4: + +```python +players = ['charles','martina','michael','florence','eli'] +print(players[1:4]) +``` + +例如,如果要提取从第3个元素到列表末尾的所有元素,可将起始索引指定为2,并省略终止索引 + + +### range函数 + +range()函数能够生成一系列的数字,例如range(1, 5)会生成1 2 3 4。 +要创建数字列表,可使用函数list()将range()的结果直接转换为列表: +```ptyhon +numbers = list(range(1, 5)) +print(numbers) + +# +print(min(numbers)) +print(max(numbers)) +print(sum(numbers)) + +``` + + +## 元组(不可变的列表) + +有时候你需要创建一系列不可修改的元素,元组可以满足这种需求。Python将不能修改的值称为不可变的,而不可变的列表被称为元组。 +元组看起来犹如列表,但使用圆括号而不是方括号来标识。定义元组后,就可以使用索引来访问其元素,就像访问列表元素一样。 +元组中的元素不能修改,访问和遍历都和列表一样。 + + +相对于列表,元组是更简单的数据结构。 +如果需要存储的一组值在程序的整个生命周期内都不变,可使用元组。 + + + +## if语句 + +```python +cars = ['audi', 'bmd', 'toyota'] +for car in cars: + if car == 'toyota': + print(car.upper()) + else: + print(car.title()) +``` +每条if语句的核心都是一个值为True或False的表达式,这种表达式被称为条件测试。 + + +```python +age = 12 +if age < 4: + print("cost $0") +elif age < 18: + print("cost $5") +else: + print("cost $10") +``` + +### match case语句 + +```python3 +match x: + case a: + xxxx + case b: + xxxx + case _: + xxx +``` +在python3.10版本新增了match case的条件判断方式,match后的对象会依次与case后的内容匹配,匹配成功则执行相应的语句,否则跳过,其中_可以匹配一切。 + + +### 相等判断 +```python +car = 'Audi' +# False +car == 'audi' +``` + +Python中使用两个等号(==)检测值是否相等,在Python中检测是否相等时区分大小写。 + +判断两个值不相等,可结合使用惊叹号和等号(!=)来判断。 + +条件语句中可包含各种数学比较,如小于<、小于等于<=、大于>、大于等于>=。 + +### 多条件检测 + +有时候需要判断两个条件都为True或只要求一个条件为True时就执行相应的操作。 + +在这些情况下,可以使用关键字and和or。 + +### 值是否包含在列表中 + +要判断特定的值是否已包含在列表中,可使用关键字in。 +```python +cars = ['audi', 'bmd', 'toyota'] +# True +print('audi' in cars) + +# False +print('audi' not in cars) +``` + + +## 字典(Key-Value) + +在Python中,字典是一系列键-值对。字典用放在花括号{}中的一些列键-值对表示。 +每个键都与一个值相关联,你可以使用键来访问与之相关联的值。 +与键相关联的值可以是数字、字符串、列表乃至字典。事实上,可将任何Python对象用作字典中的值。 + +```python +alien = {'color': 'blue', 'points': 5} +# 取值 +print(alien['color']) +print(alien['points']) +# 添加值 +alien['xPos'] = 10 +alien['yPos'] = 20 +print(alien['xPos']) +del alien['points'] +``` + +字典的其他写法: + +```python3 +dict4 = dict(name="Bob", age=20, gender="female") +dict5 = dict([("name", "Tom"), ("age", 22), ("gender", "male")]) +``` + + +### 遍历字典 + +- keys()方法返回所有的键列表。 +- items()方法返回一个键-值对列表。 +- values()方法返回一个值列表。 + +```ptyhon +alien = {'color': 'blue', 'points': 5} + +for key,value in alien.items(): + print("key: " + key) + print("value: " + str(value)) + + +for key in alien.keys(): + print("key: " + key) +``` +字典总是明确地记录键和值之间的关联关系,但获取字典的元素时,获取顺序是不可预测的。 + +这不是问题,因为通常你想要的只是获取与键相关联的正确的值。 + +要以特定的顺序返回元素,一种方法是在for循环中对返回的键进行排序。。 + +为此,可使用函数sorted()来获得特定顺序排列的键列表的副本: +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +for name in sorted(favorite_languages.keys()): + print(name.title()+",thank you for taking the poll.") +``` + +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +print("The following languages have been mentioned:") +for language in favorite_languages.values(): + print(language.title()) +``` + +这种做法提取字典中所有的值,而没有考虑是否重复。涉及的值很少时,这也许不是问题,但如果被调查者很多,最终的列表可能包含大量的重复项。为剔除重复项,可使用集合(set)。集合类似于列表,但每个元素都必须是独一无二的: +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +print("The following languages have been mentioned:") +for language in set(favorite_languages.values()):❶ + print(language.title()) +``` +通过对包含重复元素的列表调用set(),可让Python找出列表中独一无二的元素,并使用这些元素来创建一个集合。 + +## 用户输入 + +函数input()让程序暂停运行,等待用户输入一些文本。 + +获取用户输入后,Python将其存在一个变量中,以方便你使用。 +```python3 +age = input("How old are you?") +print(age) +``` +用户输入的是数字21,但我们请求Python提供age的值时,它返回的是`'21'`(用户输入的数值的字符串表示)。 + +为了解决这个问题,可以使用函数int():将数字的字符串表示转换为数值表示,如: +```python +age = input("Please Input") +print(age) +# print(age >= 18) 报错 +age = int(age) +print(age >= 18) +``` + +## while + +```python +current_number = 1 +while current_number <= 5: + print(current_number) + current_number+= 1 +``` + +同样while中也可以结合使用break、continue等,和Java基本一样。 + +## 缩进 + +在python3中,代码块的结束不像其他一些编程语言(如c、java等)使用大括号{}来明确界定,而是通过缩进来表示。PEP8建议每级缩进都使用四个空格。 + + +## 函数 + +使用关键字def来定义一个函数,定义以冒号结尾。 +跟在def xxx:后面的所有缩进行构成了函数体。 + +```python +def greet_user(username): + """函数功能的注释""" + # 返回值 + return 'Hello ' + username + +# 调用函数 +print(greet_user('jack')) +print(greet_user(username='lili')) +``` +同样,在Python中函数也支持参数的默认值。 + +上面三个引号的部分是文档字符串格式,用于简要的阐述其功能的注释。 + +### 函数的参数传递 + +- 不可变类型 + 类似c++的值传递,如整数、字符串、元祖。如fun(a),传递的只是a的值,没有影响a对象本身,比如在fun(a)内部修改a的值,只是修改了另一个复制的对象,不会影响a本身。 +- 可变类型 + 类似c++的引用传递,如列表、字典。如fun(la),则是将la真正的传过去,修改后fun外部的la也会受影响 + + +python中一切都是对象,严格意义上我们不能说值传递还是引用传递,我们应该说传可变对象和不可变对象。 + + + +### 函数列表参数副本 + +将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。 + +但是有些时候我们并不想让函数修改原始的列表。 + +为解决这个问题,可向函数传递列表的副本而不是原件;这样函数所做的任何修改都只影响副本,而丝毫不影响原件。 + +要将列表的副本传递给函数,可以像下面这样做: +``` +function_name(list_name[:]) +``` + +切片表示法[:]创建列表的副本。 + +### 函数不定参数 + +有时候,你预先不知道函数需要接受多少个实参,Python允许函数从调用语句中收集任意数量的实参。 +```python +def make_pizza(*toppings): + """打印顾客点的所有配料""" + print(toppings) +make_pizza('pepperoni') +make_pizza('mushrooms','green peppers','extra cheese') +``` +形参名`*toppings`中的星号让Python创建一个名为toppings的空元组,并将收到的所有值都封装到这个元组中。 + +现在,我们可以将这条print语句替换为一个循环,对配料列表进行遍历,并对顾客点的比萨进行描述: + +```python +def make_pizza(*toppings): + """概述要制作的比萨""" + print("\nMaking a pizza with the following toppings:") + for topping in toppings: + print("- "+topping) +make_pizza('pepperoni') +make_pizza('mushrooms','green peppers','extra cheese') +``` + + +简单来说,Python中所有的函数参数传递,统统都是基于传递对象的引用进行的。这是因为,在Python中,一切皆对象。而传对象,实质上传的是对象的内存地址,而地址即引用。 + + +看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 + + +### 解包传参 + +若函数的形参是定长参数,可以通过`*`和`**`对列表、元组、字典等解包传参。 + +```python3 +def func(a, b, c): + return a + b + c + +tuple1 = (1, 2, 3) + +print(func(*tuple)) +dict1 = {"a":1, "b":2, "c":3} + +print(func(**dict1)) +``` + + + +### 将函数存储在模块中 + +函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在被称为模块的独立文件中,再将模块导入到主程序中。import语句允许在当前运行的程序文件中使用模块中的代码。 + + +要让函数是可导入的,得先创建模块。模块是扩展名为.py的文件,包含要导入到程序中的代码。下面来创建一个包含函数make_pizza()的模块。为此,我们将文件pizza.py中除函数make_pizza()之外的其他代码都删除: + +```python +# pizza.py + +def make_pizza(size,*toppings): + """概述要制作的比萨""" + print("\nMaking a "+str(size)+ + "-inch pizza with the following toppings:") + for topping in toppings: + print("- "+topping) +``` + +接下来,我们在pizza.py所在的目录中创建另一个名为making_pizzas.py的文件,这个文件导入刚创建的模块,再调用make_pizza()两次: + +```python +# making_pizzas.py +import pizza +pizza.make_pizza(16,'pepperoni') ❶ +pizza.make_pizza(12,'mushrooms','green peppers','extra cheese') + +``` +Python读取这个文件时,代码行import pizza让Python打开文件pizza.py,并将其中的所有函数都复制到这个程序中。你看不到复制的代码,因为这个程序运行时,Python在幕后复制这些代码。你只需知道,在making_pizzas.py中,可以使用pizza.py中定义的所有函数。 + +你还可以导入模块中的特定函数,这种导入方法的语法如下: +`from modulname import funcxx` + +```python +from pizza import make_pizza +make_pizza(16,'pepperoni') +make_pizza(12,'mushrooms','green peppers','extra cheese') +``` +若使用这种语法,调用函数时就无需使用句点。由于我们在import语句中显式地导入了函数make_pizza(),因此调用它时只需指定其名称。 + +还可以使用星号(`*`)导入模块中的所有函数: + +```python +from pizza import * +make_pizza(16,'pepperoni') +make_pizza(12,'mushrooms','green peppers','extra cheese') +``` +`import *`会将模块pizza中的每个函数都复制到这个程序文件中。 +由于导入了每个函数,可通过名称来调用每个函数,而无需使用句点表示法。 +然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法: 如果模块中有函数的名称与你的项目中使用的名称相同,可能导致意想不到的结果,因为Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有的函数。 + + +### 别名 + +如果要导入的函数的名称与现有的名称冲突,或者函数的名称太长,可以通过别名的方式进行指定。 + +```python +from pizza import make_pizza as mp +mp(16,'pepperoni') +mp(12,'mushrooms','green peppers','extra cheese') +``` + +同样也可以通过as给模块指定别名: + +```python +import pizza as p + +p.make_pizza(16,'pepperoni') +p.make_pizza(12,'mushrooms','green peppers','extra cheese') +``` + +### __name__ + +在python中,__name__是一个特殊的内置变量: + +- 当一个python文件被直接运行时,该文件的__name__属性为"__main__" +- 当一个python文件作为模块被导入时,__name__属性会被设置为该模块的名称(即文件名,不包括.py后缀) + +例如有时我们在模块中写一些测试代码,当模块被其他文件导入时这些测试代码会被执行。 +例如我们在main.py中导入my_add.py中,会发现my_add中的测试代码被执行了。这个时候使用__name__ == "__main__"判断就能避免测试代码被执行。 + + + + + + +### 全局变量和局部变量 + + +定义在函数内部的变量拥有一个局部作用域,定义在函数外的变量拥有全局作用域,局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。 + +#### global关键字 + +使用global关键字修改全局变量 +```python +def function_a(): + global var1 + var1 = 200 + print("var1:", var1) + +var1 = 100 + +``` +当全局变量为可变类型时,函数内部使用global声明,也可以对其进行修改。 +```python +def function_a(): + list1[0] = 1000 + +list1 = [1, 2, 3] +``` +在函数中不使用global声明全局变量时不能修改全局变量的本质是不能修改全局变量的指向,即不能将全局变量指向新的数据。 + +- 不可变类型的全局变量其指向的数据不能修改,所以部使用global无法修改全局变量。 +- 可变类型的全局变量其指向的数据可以修改,所以不使用global也可修改全局变量。 + +#### nonlocal关键字 + +nonlocal也用作内部作用域修改外部作用域的变量的场景,不过此时外部作用域不是全局作用域,而是嵌套作用域 + +```python +def function_outer(): + var = 1 + print(var) + + def function_inner(): + nonlocal var + var = 200 + function_inner() + print(var) + + +function_outer() +``` + + + +## 类 + +在Python中,首字母大写的名称指的是类。 +根据类来创建对象叫实例化。 + +dog.py +```python +class Dog: + home = "xx" # 类属性 + + def __init__(self, name, age): + self.name = name # 实例属性 + self.age = age + + def sit(self): + print(self.name + " Sitting") + + def roll(self): + print(self.name + " Rolling") +``` + +__init__()是一个特殊的方法(构造方法),每当你根据Dog类创建新实例时,Python都会自动运行它(__init__()方法的调用时机在实例(通过__new__()被创建之后,返回调用者之前调用,一般用于初始化一些数据))。在这个方法中形参self必不可少,还必须位于其他形参的前面。 + +为什么必须在方法定义中包含形参self呢?因为Python调用这个__init__()方法来创建Dog实例时,将自动传入实参self。 + +每个与类相关联的方法调用都自动传入实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。 + + + +```python +import dog + +dog = dog.Dog("xiaohei", 1) +# 属性 +print(dog.name) +# 方法 +dog.sit() +``` + +类属性可以直接通过类名.类属性调用也可以通过实例对象.类属性调用,如`Dog.home或dog.home` + +```python +class Person: + home = "class home" + + def __init__(self, name) -> None: + self.name = name + + +person = Person("name") +print(person.name) +print(person.home) +person.home = "object home" +print(person.home) +print(Person.home) +Person.home = "new class home" +print(Person.home) + +# 结果 +name +class home +object home +class home +new class home + +``` +可以看到所有类的实力都会共享同一个类属性,如果通过对象实例.类属性调用则会直接创建一个实例属性(并不是修改了类属性的值) + +#### 动态添加属性和方法 + +```python +class Person: + def __init__(self, name: str) -> None: + self.name = name + +p = Person("John") +p.age = 20 +print(p.name) +print(p.age) + +``` + + + +### 方法 + +Python的类中有三种方法: 实例方法、静态方法、类方法。 + +- 实例方法在类中定义,第一个参数是self,代表实例本身 +- 实例方法只能被实例对象调用 +- 可以访问实例属性、类属性、类方法 + +#### 类方法 + +- 类方法通过@classmethod标注,方法的第一个参数是cls +- 类方法可以通过类来调用也可以通过实例来调用 +- 可以访问类属性 +- 类方法可以在不创建实例的情况下调用,通过类名直接调用,非常方便,适合一些和类整体相关的操作 + + +```python +class Dog: + + @classmethod + def wangwang(cls): + print"wangwang" + + +``` + +#### 静态方法 + +- 静态方法通过@staticmethod定义 +- 不访问实例属性或类属性,只依赖传入的参数 +- 可以通过类名或实例调用,但它不会访问类或实例的内部信息,更像一个工具函数 + + +#### 特殊方法 + + +方法名中有两个前缀下划线和两个后缀下划线的方法为特殊方法,也叫魔法方法。 +上面的__init__()就是一个特殊方法,这些方法会在进行特定的操作时自动被调用。 + +几个常见的特殊方法: + +- __new__(): 对象实例化时第一个调用的方法 +- __init__(): 类的初始化方法 +- __del__(): 对象的销毁器,定义了当对象被垃圾回收时的行为,使用del xxx时不会主动调用__del__(),除非此时引用计数=0 +- __str__(): 定义了对类的实例调用str()时的行为 +- __repr__(): 定义了对类的实例调用repr()时的行为,str()和repr()最主要的差别在于目标用户。repr()的作用是产生机器可读的输出,而str()则产生人类可读的输出。 +- __getattribute__():属性访问拦截器,定义了属性被访问前的操作。 + + + + + + +### 继承 + +一个类继承另一个类时,它将自动获得另一个类的所有属性和方法。 + +创建子类实例时,Python首先需要完成的任务是给父类的所有属性赋值。因此,子类的__init__()方法需要调用父类的方法。 + +```python +class Car: + def __init__(self, make, model, year): + self.make = make + self.model = model + self.year = year + + def get_des(self): + name = str(self.year) + str(self.make) + str(self.model) + return name.title() + + +class ElectricCar(Car): + def __init__(self, make, model, year, battery): + super().__init__(make, model, year) + self.battery = battery + + # 重写父类方法 + def get_des(self): + name = str(self.year) + str(self.make) + str(self.model) + str(self.battery) + return name.title() + def get_battery(self): + print("battery : " + str(self.battery)) + + +tesla = ElectricCar('Tesla', 'Model S', 2021, 80) +print(tesla.get_des()) +tesla.get_battery() +``` + +- 创建子类时,父类必须包含在当前文件中,且位于子类前面。 +- 定义子类时,必须在括号内指定父类的名称 +- 方法__init__()接受创建子类实例所需的信息 + +super()是一个特殊函数,帮助Python将父类和子类关联起来。这行代码让子类包含父类的所有属性。 + + +多继承: 调用方法时先在子类中查找,若不存在则从左到右依次查找父类中是否包含方法。 + +#### 私有属性和方法 + +有时为了限制属性和方法只能在类内访问,外部无法访问。或父类中某些属性和方法不希望被子类继承,可以将其私有化: + +- 单下划线: 非公开API + + 大多数python代码都遵循这样一个约定,有一个前缀下划线的变量或方法应被视为非公开的API,例如_var1。但这种约定不具有强制力。 + +- 双下划线: 名称改写 + 有两个前缀下划线,并至多一个后缀下划线的标识符,例如__x,会被改写为_类名__x,只有在类内部可以通过__x访问,其他地方无法访问或只能通过_类名__x访问。 + + +#### property装饰器 + +可通过@property装饰器将一个方法转换为属性来调用。转换后可直接使用.方法名来使用,而无需使用.方法名()。 + +注意: @property修饰的方法不要和变量重名,否则可能导致无限递归 +#### 只读属性 + +```python +class Person: + def __init__(self, name: str) -> None: + self.__name = name + + @property + def name(self): + return self.__name + +p = Person("zhangsan") +print(p.name) + +.name = "lisi" # 报错 +``` + +#### 读写属性 + +使用属性名.setter装饰 + +```python +class Person: + def __init__(self, name: str) -> None: + self.__name = name + + @property + def name(self): + return self.__name + + @name.setter + def name(self, name: str) -> None: + self.__name = name + +p = Person("zhangsan") +print(p.name) + +p.name = "lisi" +print(p.name) +``` + + + + +## 标准库 + +Python标准库是一组模块,安装的Python都包含它。 +类名应采用驼峰命名法,即将类名中的每个单词的首字母都大写,而不使用下划线。实例名和模块名都采用小写格式,并在单词之间加上下划线。 +每个模块也都应包含一个文档字符串,对其中的类可用于做什么进行描述。 + + +### 读取文件 + +一次性读取整个文件: +```python +with open('test.txt') as file_object: + contents = file_object.read() + print(contents) +``` + +要以每次一行的方式检查文件,可对文件对象使用for循环: + +```python +filename = 'test.txt' +with open(filename) as file_object: + for line in file_object: + print(line) +``` +这里使用了关键字with,让Python负责妥善地打开和关闭文件。 +使用关键字with时,open()返回的文件对象只在with代码块内可用。 + + +要将文本写入文件,你在调用open()时需要提供另一个实参,告诉Python你要写入打开的文件。 + +```python +filename = 'test.txt' + +with open(filename, 'w') as fo: + fo.write("Hello World") +``` +调用open()时提供了两个实参: + +- 第一个实参是要打开的文件的名称 +- 第二个实参('w')告诉Python,我们要以写入模式打开这个文件。 + +打开文件时,可指定读取模式('r')、写入模式('w')、附加模式('a')或让你能够读取和写入文件的模式('r+')。如果你省略了模式实参,Python将以默认的只读模式打开文件。 +如果你要写入的文件不存在,函数open()将自动创建它。然而,以写入('w')模式打开文件时千万要小心,因为如果指定的文件已经存在,Python将在返回文件对象前清空该文件。 +注意 Python只能将字符串写入文本文件。要将数值数据存储到文本文件中,必须先使用函数str()将其转换为字符串格式。 + +如果你要给文件添加内容,而不是覆盖原有的内容,可以附加模式打开文件。你以附加模式打开文件时,Python不会在返回文件对象前清空文件,而你写入到文件的行都将添加到文件末尾。如果指定的文件不存在,Python将为你创建一个空文件。 + + +### 异常 + +异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。 + +```python +try: + 可能发生异常的代码 +except 异常类型1 as 变量名1: + 异常1处理代码 +except 异常类型2 as 变量名2: + 异常2处理代码 +else: + 没有异常时执行的代码 +finally: + 无论是否发生异常都会执行的代码 +``` + +#### 抛出异常 + +当想在代码中明确表示发生了错误或异常时,可以使用raise来抛出异常。 +```python +def int_add(a, b): + if isinstance(a, int) and isinstance(b, int): + return a + b + else: + raise TypeError("参数类型错误") +``` + +### with关键字 + + +python中的with语句用于异常处理,封装了try except finally编码范式,提供了一种简洁的方式来确保资源的正确获取和释放,同时处理可能发生的异常,提高了易用性,使代码更清晰、更具可读性,简化了文件流等公共资源的管理 + +```python +with expression as variable: + # 代码块 +``` + +- expression: 通常是一个对象或函数调用,该对象需要一个上下文管理器,即实现了__enter__和__exit__方法 + +- variable: 是可选的,用于存储expression的__enter__方法的返回值。 + +工作原理: + +- 使用with关键字系统会自动调用f.close()方法,with的作用等效于try finally语句。 - 当执行with语句时,会调用expression对象的__enter__方法 +- __enter__方法的返回值可以被存储在variable中(如果有),以供with代码块中使用 +- 然后执行with语句内部的代码块 +- 无论在代码块中是否发生异常,都会调用expression对象的__exit__方法,以确保资源的释放或清理工作,这类似于try - except -finally中的finally语句。 + + + + + +### 分割字符串 + +方法split()以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中。 + +```python +title = "Alice in Wonderland" +title.split() +``` + +['Alice','in','Wonderland'] + + +### pass + +Python有一个pass语句,可在代码块中使用它来让Python什么都不要做。 + +```python +def donothing(): + pass #空语句 + +``` + + +pass语句如果啥事都不做,那要它有何用呢?实际上,pass可以作为一个占位符来用——视为未成熟代码的“预留地”。比如说,如果我们还没想好函数的内部实现,就可以先放一个pass,让代码先跑起来。 + +### json + +函数json.dump()接受两个实参:要存储的数据以及可用于存储数据的文件对象。下面演示了如何使用json.dump()来存储数字列表: + +```python +import json +numbers = [2,3,5,7,11,13] +filename = 'numbers.json' +with open(filename,'w') as f_obj: + json.dump(numbers,f_obj) +``` + +使用json.load()将这个列表读取到内存中: +```python +import json +filename = 'numbers.json' +with open(filename) as f_obj: + numbers = json.load(f_obj) +print(numbers) +``` + +### 网络请求 + +Web API是网站的一部分,用于与使用非常具体的URL请求特定信息的程序交互。这种请求称为API调用。请求的数据将以易于处理的格式(如JSON或CSV)返回。依赖于外部数据源的大多数应用程序都依赖于API调用,如集成社交媒体网站的应用程序。 + + + +requests包让Python程序能够轻松地向网站请求信息以及检查返回的响应。要安装requests,请执行类似于下面的命令: + +$pip install --user requests +或者直接在ide中点击修复安装就可以: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/python_install_requests.png) + +```python +import requests +# 执行API调用并存储响应 +url = 'https://api.github.com/search/repositories?q=language:python&sort=stars' +r = requests.get(url) +print("Status code:",r.status_code) +# 将API响应存储在一个变量中 +response_dict = r.json() +# 处理结果 +print(response_dict.keys()) +``` + +执行结果为: +``` +Status code: 200 +dict_keys(['total_count', 'incomplete_results', 'items']) +``` + +这个API返回JSON格式的信息,因此我们使用方法json()将这些信息转换为一个Python字典。我们将转换得到的字典存储在response_dict中。 +最后,我们打印response_dict中的键。 + + + + +在Python中,还可以定义可变参数。可变参数也称不定长参数,即传入函数中的实际参数可以是零个、一个、两个到任意个。 + +Python中的可变参数的表现形式为,在形参前添加一个星号(*),意为函数传递过来的实参个数不定,可能为0个、1个,也可能为n个(n≥2)。需要注意的是,不管可变参数有多少个,在函数内部,它们都被“收集”起来并统一存放在以形参名为某个特定标识符的元组之中。因此,可变参数也被称为“收集参数”。 + + +除了用单个星号(*)表示可变参数,其实还有另一种标定可变参数的形式,即用两个星号(**)来标定。通过前文的介绍,我们知道,一个星号(*)将多个参数打包为一个元组,而两个星号(**)的作用是什么呢?它的作用就是把可变参数打包成字典模样。 + +这时调用函数则需要采用如“arg1=value1,arg2=value2”这样的形式。等号左边的参数好比字典中的键(key)[插图],等号右边的数值好比字典中的值(value)​, + +```python +def varFun(**x): + if len(x) == 0: + print("None") + else: + print(x) + + +varFun(a = 1, b = 3) + + +``` + +定义可变参数时,主要有两种形式,一种是*parameter,另一种是**parameter。下面分别进行介绍。 +1.*parameter +这种形式表示接收任意多个实际参数并将其放到一个元组中。 +2.**parameter +这种形式表示接收任意多个类似关键字参数一样显式赋值的实际参数,并将其放到一个字典中。 + + +除了用等号给可变关键字参数赋值,事实上,我们还可以直接用字典给可变关键字参数赋值: + +```python +def some_kwargs(name, age, sex): + print(f"name: ${name}") + +kdic = {'name' : 'Alice', 'age' : 11, 'sex' : 'nv'} +some_kwargs(**kdic) + +``` + + +在Python中,可以通过@property(装饰器)将一个方法转换为属性,从而实现用于计算的属性。将方法转换为属性后,可以直接通过方法名来访问方法,而不需要再添加一对小括号“()”,这样可以让代码更加简洁。 + + +#### lambda + +Python使用lambda来定义匿名函数,所谓匿名,指其不用def的标准形式定义函数。 + + +Lambda 表达式的基本语法是:`lambda 参数列表: 表达式`。 +关键字与参数:以 lambda关键字开头,后跟参数列表(如 x或 x, y),参数之间用逗号分隔。这些参数是函数的输入。 +表达式与返回值:冒号后面是一个单一的表达式。这个表达式会被求值,其结果就是 lambda 函数的返回值。注意,lambda 函数体内只能有一个表达式,不能包含复杂的语句或多行代码块。 +Lambda 函数是匿名的,也就是说,它们没有像用 def定义的函数那样的名称。它们的优势在于简洁,特别适合定义那些逻辑简单、可能只使用一次的短小函数。 + + +### var1 *= 2与var1 = var1 * 2的区别 + + +- var1 *= 2使用原地址 +- var1 = var1 * 2开辟了新的空间 +- 同样的对于类似,var1 += 2和var1 = var1 + 2也是同理 + +```python +def test(var1): + print("函数内var1 id:", id(var1)) + var1 *= 2 + print("var1 *= 2后函数内var1 id:", id(var1)) + var1 = var1 * 2 + print("var1 = var1 * 2后函数内var1 id:", id(var1)) + +函数内var1 id: 2302584035712 +var1 *= 2后,函数内var1 id: 2302584035712 +var1 = var1 * 2后,函数内var1 id: 2302584033664 +``` + + + + + +---- + + + + + + +计算机高级运行方式: + +- 编译型: c、c++ +- 解释型: python、JavaScript +- 先编译再解释: java + + +其实python也是先编译再解释,但是python的编译时机和java的不一样。 +- java是在代码解释执行前,先进行预编译,编译后生成字节码文件存在硬盘中。 +- python是在对代码进行逐行解释执行的时候进行的编译操作,默认生成的字节码文件存在内存中。 + + diff --git a/JavaKnowledge/shell.md b/JavaKnowledge/shell.md new file mode 100644 index 00000000..1c2fb339 --- /dev/null +++ b/JavaKnowledge/shell.md @@ -0,0 +1,101 @@ +shell +--- + + + + +### ls: + +ls x* y*: 过滤文件中x和y开头的文件 +ls -F: 区分文件还是目录 +ls -l: 显示详细信息 + +### touch: 创建或修改文件时间 + + +### cp: 复制文件 + +cp sourceFile destinationFile,但是如果源文件存在会直接被覆盖,也不会提醒, +如果想要提醒需要加上-i, 例如cp -i 1.txt 2.txt + + +### mv移动重新命名 + +用法和cp一致 + + +### rm删除 + +rm -i xxx: -i会询问是否真的要删除 +rm -f xxx: 强制删除,不会询问 + +注意 对于rm命令,-r选项和-R选项的效果是一样的,都可以递归地删除目录中的文件。shell命令很少会对相同的功能使用大小写不同的选项。 +一口气删除目录树的最终解决方案是使用rm -rf命令。该命令不声不响,能够直接删除指定目录及其所有内容。当然,这肯定是一个非常危险的命令,所以务必谨慎使用,并再三检查你要进行的操作是否符合预期。 + + +### mkdir 创建目录 + +mkdir命令的-p选项可以根据需要创建缺失的父目录。父目录是包含目录树中下一级目录的目录。 + +### rmdir 删除空目录 + +### file + +file命令是一个方便的小工具,能够探测文件的内部并判断文件类型: +$ file .bashrc +.bashrc: ASCII text + + +### cat 显示文本文件 + +cat fileName + +-n 加上行号 + +### more +cat的缺点是其开始运行后无法控制后续的操作,为了解决这个问题,有了more命令。 + +more命令会显示文件内容,但会在显示每页数据之后暂停下来。 + +### ps 显示当前用户进程 + +ps -ef显示系统中运行的所有进程 + +### top 实时监测进程 + +ps命令虽然在收集系统中运行进程的信息时非常有用,但也存在不足之处:只能显示某个特定时间点的信息。如果想观察那些被频繁换入和换出内存的进程,ps命令就不太方便了。这正是top命令的用武之地。与ps命令相似,top命令也可以显示进程信息,但采用的是实时方式。 + + + +### kill pid, 通过pid发送信号 + +### pkill pname: pkill命令可以使用程序名代替PID来终止进程。除此之外,pkill命令也允许使用通配符。 + + +### grep数据搜索 + +经常需要在大文件中搜索 + +### gzip压缩 + +gzip: 压缩 +gzcat: 查看压缩过的文本文件内容 +gunzip:解压 + +### tar归档 + +ar命令最开始是用于将文件写入磁带设备以作归档,但它也可以将输出写入文件,这种用法成了在Linux中归档数据的普遍做法。tar命令的格式如下 + +// 该命令创建了一个名为test.tar的归档文件,包含目录test和test2的内容。 +tar -cvf test.tar test/ test2/ + + +### 切换到root用户 + + +`sudo su -` + + - sudo : super user doing + - su : switch user + - `-` : root用户 + diff --git "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" new file mode 100644 index 00000000..8894decc --- /dev/null +++ "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" @@ -0,0 +1,257 @@ +# volatile和Synchronized + +## 内存模型 + +内存模型:英文名 Memory Model,它是一个老古董了。它是与计算机硬件有关的一个概念。那么,我先介绍下它和硬件到底有啥关系。 + +计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存。但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。可是,不能因为内存的读写速度慢,就不发展CPU技术了,所以人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。 + +那么,程序的执行过程就变成了:当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L3),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。 + +这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。 + +那么,在有了多级缓存之后,程序的执行就变成了: + +当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。 + +> 这就像一家创业公司,刚开始,创始人和员工之间工作关系其乐融融,但是随着创始人的能力和野心越来越大,逐渐和员工之间出现了差距,普通员工原来越跟不上CEO的脚步。老板的每一个命令,传到到基层员工之后,由于基层员工的理解能力、执行能力的欠缺,就会耗费很多时间。这也就无形中拖慢了整家公司的工作效率。 +> +> 之后,这家公司开始设立中层管理人员,管理人员直接归CEO领导,领导有什么指示,直接告诉管理人员,然后就可以去做自己的事情了。管理人员负责去协调底层员工的工作。因为管理人员是了解手下的人员以及自己负责的事情的。所以,大多数时候,公司的各种决策,通知等,CEO只要和管理人员之间沟通就够了。 +> +> 随着公司越来越大,老板要管的事情越来越多,公司的管理部门开始改革,开始出现高层,中层,底层等管理者。一级一级之间逐层管理。 +> +> 公司也分很多种,有些公司只有一个大Boss,他一个人说了算。但是有些公司有比如联席总经理、合伙人等机制。 +> +> 单核CPU就像一家公司只有一个老板,所有命令都来自于他,那么就只需要一套管理班底就够了。 +> +> 多核CPU就像一家公司是由多个合伙人共同创办的,那么,就需要给每个合伙人都设立一套供自己直接领导的高层管理人员,多个合伙人共享使用的是公司的底层员工。 +> +> 还有的公司,不断壮大,开始拆分出各个子公司。各个子公司就是多个CPU了,互相使用没有共用的资源。互不影响。 + +随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。 + +**单线程**:cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 + +**单核CPU,多线程**:进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 + +**多核CPU,多线程**:每个核都至少有一个L1缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的cache中保留一份共享内存的缓存。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 + +在CPU和主存之间增加缓存,在多线程场景下就可能存在**缓存一致性问题**,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。 + +> 如果这家公司的命令都是串行下发的话,那么就没有任何问题。 +> +> 如果这家公司的命令都是并行下发的话,并且这些命令都是由同一个CEO下发的,这种机制是也没有什么问题。因为他的命令执行者只有一套管理体系。 +> +> 如果这家公司的命令都是并行下发的话,并且这些命令是由多个合伙人下发的,这就有问题了。因为每个合伙人只会把命令下达给自己直属的管理人员,而多个管理人员管理的底层员工可能是公用的。 +> +> 比如,合伙人1要辞退员工a,合伙人2要给员工a升职,升职后的话他再被辞退需要多个合伙人开会决议。两个合伙人分别把命令下发给了自己的管理人员。合伙人1命令下达后,管理人员a在辞退了员工后,他就知道这个员工被开除了。而合伙人2的管理人员2这时候在没得到消息之前,还认为员工a是在职的,他就欣然的接收了合伙人给他的升职a的命令。 + +### 处理器优化和指令重排 + +上面提到在CPU和主存之间增加缓存,在多线程场景下会存在**缓存一致性问题**。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是**处理器优化**。 + +除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做**指令重排**。 + +可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。 + +> 关于员工组织调整的情况,如果允许人事部在接到多个命令后进行随意拆分乱序执行或者重排的话,那么对于这个员工以及这家公司的影响是非常大的。 + +### 并发编程问题 + +并发编程,为了保证数据的安全,需要满足以下三个特性: + +**原子性**是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。 + +**可见性**是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 + +**有序性**即程序执行的顺序按照代码的先后顺序执行。 + +有没有发现,**缓存一致性问题**其实就是**可见性问题**。而**处理器优化**是可以导致**原子性问题**的。**指令重排**即会导致**有序性问题**。所以,后文将不再提起硬件层面的那些概念,而是直接使用大家熟悉的原子性、可见性和有序性。 + +前面提到的,缓存一致性问题、处理器器优化的指令重排问题是硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢? + +最简单直接的做法就是废除处理器和处理器的优化技术、废除CPU缓存,让CPU直接和主存交互。但是,这么做虽然可以保证多线程下的并发问题。但是,这就有点因噎废食了。 + +所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。 + +**为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范**。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。 + +内存模型解决并发问题主要采用两种方式:**限制处理器优化**和**使用内存屏障**。 + +## Java内存模型 + +**Java内存模型**(java Memory Model,JMM)描述了Java程序中各种变量(**线程共享变量**)的访问规则,以及在JVM中将变量**存储到内存**和从**内存中读取出变量**这样的底层细节。 + +Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存(每个线程都分配有单独的处理器缓存,用这些处理器缓存去缓存一些数据,就可以不用再次访问主内存去获取相应的数据,这样就可以提高效率)。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。对于共享普通变量来说,约定了变量在工作内存中发生变化了之后,必须要回写到工作内存(**迟早要回写但并非马上回写**),*但对于volatile变量则要求工作内存中发生变化之后,必须马上回写到工作内存,而线程读取volatile变量的时候,必须马上到工作内存中去取最新值而不是读取本地工作内存的副本*,此规则保证了前面所说的“当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动”。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_mm.png) + +这里面提到的主内存和工作内存,读者可以简单的类比成计算机内存模型中的主存和缓存的概念。特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。 + +**所以,再来总结下,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。** + +## volatile + +作用:使变量在多个线程间可见(可见性),能够保证volatile变量的可见性,但不能保证volatile变量复合操作的原子性。 + +`Java`语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而在这个过程中,变量的新值对其他线程是不可见的.而且只当线程进入或者离开同步代码块时才与共享成员变量 +的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。也就是说每个线程都有一个自己的本地内存空间,在线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作,当对该变量操作完后,在某个时间再把变量刷新回主内存。 + +- volatile如何实现内存可见性。深入来说:通过加入**内存屏障**和**禁止重排序优化**来实现的 +- 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令 +- 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令 + +当volatile修饰一个变量i在线程1中从1发生改变成2时,这时线程1会做两件事: + +1. **更新主内存。** +2. **向CPU总线发送一个修改信号。** + +**这时监听CPU总线的处理器会收到这个修改信号后,如果发现修改的数据自己缓存了,就把自己缓存的数据失效掉。这样其它线程访问到这段缓存时知道缓存数据失效了,需要从主内存中获取。这样所有线程中的共享变量i就达到了一致性。** + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_mm_1.jpg) + +**所以volatile也可以看作线程间通信的一种廉价方式。** + +### volatile关键字的非原子性 + +所谓原子性,就是某系列的操作步骤要么全部执行,要么都不执行。 + +比如,变量的自增操作 i++,分三个步骤: + +- 从内存中读取出变量 i 的值 +- 将i的值加1 +- 将加1后的值写回内存 + +这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。 + +volatile修饰的变量并不保证对它的操作(自增)具有原子性。(对于自增操作,可以使用JAVA的原子类AutoicInteger类保证原子自增) + +比如,假设i自增到5,线程A从主内存中读取i,值为5,将它存储到自己的线程空间中,执行加1操作,值为6。此时,CPU切换到线程B执行,从主从内存中读取变量i的值。由于线程A还没有来得及将加1后的结果写回到主内存,线程B就已经从主内存中读取了i,因此,线程B读到的变量i值还是5。 + +相当于线程B读取的是已经过时的数据了,从而导致线程不安全性。这种情形在《Effective JAVA》中称之为“安全性失败” + +**综上,仅靠volatile不能保证线程的安全性。(原子性)** + +### volatile禁止指令重排序 + +代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化 + +1. **编译器优化的重排序(编译器优化)** +2. **指令级并行重排序(处理器优化)** +3. **内存系统的重排序(处理器优化)** + +volatile禁止指令重排序,典型的应用是单例模式中的双重检查机制。 + +由于synchronized是一个重量级锁,会影响运行效率。双重检查机制中,先判断单例变量是否为null,再进入同步代码块。这样只在第一次获取单例变量时,会有效率影响。 + +```java +public class DoubleCheckSingleton { + + private volatile static DoubleCheckSingleton singleton; + + private DoubleCheckSingleton() {} + + public static DoubleCheckSingleton getInstance() { + if(singleton == null) { + synchronized (singleton) { + if(singleton == null) { + singleton = new DoubleCheckSingleton(); + } + } + } + return singleton; + } + +} +``` + +单例变量需要用volatile修饰,否则,在new单例对象时会出现问题。使用new来创建一个对象可以分解为如下的3行伪代码: + +```java +memory = allocate(); //1.分配对象的内存空间 +ctorInstance(memory); //2.初始化对象 +instance = memory; //3.设置instance指向刚分配的内存地址 +``` + +上面3行伪代码中的2和3之间可能会发生重排序,排序后的执行顺序如下: + +```java +memory = allocate(); //1.分配对象的内存空间 +instance = memory; //3.设置instance指向刚分配的内存地址,此时对象还没有初始化,但instance == null 判断为false +ctorInstance(memory); //2.初始化对象 +``` + +指令重排序后,在多线程情况下,可能会发生A线程正在new对象,执行了3,但还没有执行2。此时B线程进入方法获取单例对象,执行同步代码块外的非空判断,发现变量非空,但此时对象还未初始化,B线程获取到的是一个未被初始化的对象。使用volatile修饰后,禁止指令重排序。即,先初始化对象后,再设置instance指向刚分配的内存地址。这样就就不存在获取到未被初始化的对象。 + + + +那为何说它禁止指令重排序呢?从硬件架构上讲,指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并不是说指令任意重排,处理器必须能正确处理指令依赖情况保障程序能得出正确的执行结果。譬如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1和指令2是有依赖的,它们之间的顺序不能重排——(A+10)*2与A*2+10显然不相等,但指令3可以重排到指令1、2之前或者中间,只要保证处理器执行后面依赖到A、B值的操作时能获取正确的A和B值即可。所以在同一个处理器中,重排序过的代码看起来依然是有序的。因此,lock addl$0x0,(%esp)指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果。 + + + +## synchronized + +**synchronized实现同步的基础是:Java中的每个对象都可作为锁。所以synchronized锁的都对象,只不过不同形式下锁的对象不一样。** + +- **对于普通同步方法,锁的是当前实例对象。** +- **对于静态同步方法,锁的是当前类的Class对象。** +- **对于同步方法块,锁是Synchronized括号里配置的对象。** + +`synchronized`为一段操作或内存进行加锁,它具有互斥性。当线程要操作被`synchronized`修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。 +它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。 + +- 当两个并发线程访问同一个对象中的这个`synchronized(this)`同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 +- 然而,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,另一个线程仍然可以访问该`object`中的非`synchronized(this)`同步代码块。 +- 尤其关键的是,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,其他线程对`object`中所有其它`synchronized(this)`同步代码块的访问将被阻塞。 + +**在JVM规范中规定了synchronized是通过Monitor对象来实现方法和代码块的同步,但两者实现细节有点一不样。代码块同步是使用monitorenter和monitorexit指令,方法同步是使用另外一种方法实现,细节JVM规范并没有详细说明。但是,方法的同步同样可以使用这两指令来实现。** + +**monitorenter指令是编译后插入到同步代码块的开始位置,而monitorexit指令是插入到方法结束处和异常处。JVM保证了每个monitorenter都有对应的monitorexit。任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,对象将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象对应monitor的所有权,即尝试获得对象的锁。** + +### synchronized缺点 + +#### 有性能损耗 + +虽然在JDK 1.6中对synchronized做了很多优化,如如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等,但是他毕竟还是一种锁。以上这几种优化,都是尽量想办法避免对Monitor进行加锁,但是,并不是所有情况都可以优化的,况且就算是经过优化,优化的过程也是有一定的耗时的。所以,无论是使用同步方法还是同步代码块,在同步操作之前还是要进行加锁,同步操作之后需要进行解锁,这个加锁、解锁的过程是要有性能损耗的。 + +Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是: + +> 对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。 +> +> 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。 + +#### 产生阻塞 + +基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待。 + +![img](http://47.103.216.138/wp-content/uploads/2019/08/15660298698995.jpg) + +所以,synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。 + +## volatile与Synchronized的区别: + +- synchronized通过加锁的方式,使得其在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成。 +- volatile通过在volatile变量的操作前后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性。 +- volatile关键字是无法保证原子性的,而synchronized通过monitorenter和monitorexit两个指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,即可保证不会出现CPU时间片在多个线程间切换,即可保证原子性 +- volatile是变量修饰符,而synchronized则作用于一段代码或方法。 +- volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。 + +一句话,那什么时候才能用volatile关键字呢?(千万记住了,重要事情说三遍,感觉这句话过时了) + +> 如果写入变量值不依赖变量当前值,那么就可以用 volatile +> +> 如果写入变量值不依赖变量当前值,那么就可以用 volatile +> +> 如果写入变量值不依赖变量当前值,那么就可以用 volatile + +比如上面 count++ ,是获取-计算-写入三步操作,也就是依赖当前值的,所以不能靠volatile 解决问题。 + +参考: + +- [再有人问你Java内存模型是什么,就把这篇文章发给他](https://www.hollischuang.com/archives/2550) +- [原子性、可见性以及有序性](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%8E%9F%E5%AD%90%E6%80%A7%E3%80%81%E5%8F%AF%E8%A7%81%E6%80%A7%E4%BB%A5%E5%8F%8A%E6%9C%89%E5%BA%8F%E6%80%A7.md) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\212).md" "b/JavaKnowledge/\345\211\221\346\214\207Offer(\344\270\212).md" similarity index 98% rename from "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\212).md" rename to "JavaKnowledge/\345\211\221\346\214\207Offer(\344\270\212).md" index cdb8a89f..9220e3c9 100644 --- "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\212).md" +++ "b/JavaKnowledge/\345\211\221\346\214\207Offer(\344\270\212).md" @@ -13,6 +13,7 @@ 具体实现: - 饿汉式 + ```java public class Singleton { private Singleton() { @@ -26,6 +27,7 @@ } ``` - 懒汉式 + ```java public class Singleton { private Singleton() { @@ -47,6 +49,7 @@ } ``` - 枚举 + ```java public enum Singleton { INSTANCE; @@ -56,33 +59,35 @@ } ``` - 我这里写一种自我感觉是单例最完美的实现方式:   - ```java - public class Singleton { - // Private constructor prevents instantiation from other classes - private Singleton() { } - - /** - * SingletonHolder is loaded on the first execution of Singleton.getInstance() - * or the first access to SingletonHolder.INSTANCE, not before. - */ - private static class SingletonHolder { - public static final Singleton INSTANCE = new Singleton(); - } + - 我这里写一种自我感觉是单例最完美的实现方式 +    + ```java + public class Singleton { + // Private constructor prevents instantiation from other classes + private Singleton() { } - public static Singleton getInstance() { - return SingletonHolder.INSTANCE; + /** + * SingletonHolder is loaded on the first execution of Singleton.getInstance() + * or the first access to SingletonHolder.INSTANCE, not before. + */ + private static class SingletonHolder { + public static final Singleton INSTANCE = new Singleton(); + } + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } } - } - ``` + ``` 3. 二维数组中的查找 题目描述:一个二维数组,每一行从左到右递增,每一列从上到下递增.输入一个二维数组和一个整数,判断数组中是否含有整数。 分析: + ``` 1 6 11 5 9 15 7 13 20 + ``` 假设我们要找7,那怎么找呢? 我们先从第一行找,从后往前找,因为他是递增的,先是11,这里11>7所以肯定不是第三列的。这时候我们就找第二列, @@ -186,8 +191,8 @@ if (headNode.next != null) { printListReverse(headNode.next); } + System.out.println(headNode.data); } - System.out.println(headNode.data); } ``` @@ -248,7 +253,7 @@ ``` 7. 用两个栈实现队列 - 用两个栈实现一个队列,实现对了的两个函数`appendTail`和 + 用两个栈实现一个队列,实现队列的两个函数`appendTail`和 `deleteHead`,分别完成在队列尾插入结点和在队列头部删除结点的功能。 思路: 栈是啥?栈是先进后出,因为栈是一个出口啊,先进入的被压在最下面了,出要从上面开始出,也就是吃了吐出来。 队列是啥?两头的,就想管道一样,先进先出。不雅的说,就是吃了拉出来。 @@ -460,7 +465,7 @@ ``` 12. 打印 1 到最大的 n 位数 输入数字n,按顺序打印出从1最大的的n位数十进制数。比如输入3,则打印出1,2,3一直到最大的3位数即999. - 思路: 1位数就是10-1,两位数就是10*10-1三位数就是10*10*10-1 + 思路: 1位数就是`10-1`,两位数就是`10*10-1`三位数就是`10*10*10-1` ```java public void print1ToMaxOfNDigits(int n) { @@ -515,7 +520,7 @@ } } } -``` + ``` 13. 在O(1)时间删除链表节点 给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间删除该节点。 @@ -746,7 +751,6 @@ } } ``` - 17. 合并两个排序的链表 输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按 照递增排序的。 @@ -892,7 +896,6 @@ BinaryTreeNode rightNode; } ``` - 19. 二叉树的镜像 请完成一个函数,输入一个二叉树,该函数输出它的镜像。 思路:什么事镜像? 就像照镜子一样。打个比方现在的数是 @@ -1171,7 +1174,6 @@ } } ``` - 23. 从上往下打印二叉树 从上往下打印二叉树的每个结点,同一层的结点按照从左到右的顺序打印。 思路: 每一次打印一个结点的时候,如果该结点有子节点,把该结点的子节点放到一个队列的尾。接下来到队列的头部取出最早进入队列的结点,重复前面打印操作,直到队列中所有的结点都被打印出为止。 diff --git "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" "b/JavaKnowledge/\345\211\221\346\214\207Offer(\344\270\213).md" similarity index 99% rename from "Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" rename to "JavaKnowledge/\345\211\221\346\214\207Offer(\344\270\213).md" index 50e15bf7..4df119ef 100644 --- "a/Java\345\237\272\347\241\200/\345\211\221\346\214\207Offer(\344\270\213).md" +++ "b/JavaKnowledge/\345\211\221\346\214\207Offer(\344\270\213).md" @@ -8,7 +8,8 @@ 思路: 在后序遍历得到的序列中,最后一个数字是树的根节点的值。 数组中前面的数字可以分为两部分:第一部分是左子树结点的值, 它们都比根节点的值小;第二部分是右子树结点的值,他们都比根节点的值大。 - ```java + + ``` public class Problem24 { public static void main(String[] args) { int[] array = { 5, 7, 6, 9, 11, 10, 8 }; @@ -60,12 +61,12 @@ 25. 二叉树中和为某一值的路径 输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶结点所经过的所有的结点形成一条路径。 思路: - ```java + + ``` public class Problem25 { public static void main(String args[]) { BinaryTreeNode root1 = new BinaryTreeNode(); BinaryTreeNode node1 = new BinaryTreeNode(); - BinaryTreeNode node2 = new BinaryTreeNode(); BinaryTreeNode node3 = new BinaryTreeNode(); BinaryTreeNode node4 = new BinaryTreeNode(); diff --git "a/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" "b/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" new file mode 100644 index 00000000..a010cb92 --- /dev/null +++ "b/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" @@ -0,0 +1,389 @@ +动态代理 +=== + +有关代理模式已经动态代理和静态代理的区别请查看另一篇文章[设计模式](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) + +动态代理我们平时用的其实并不多,但是作为`Android`开发,你肯定知道`Retrofit`,而`Retrofit`就是基于动态代理实现。 + +动态代理的类和接口 +--- + +- `Proxy`:动态代理机制的主类,提供一组静态方法为一组接口动态的生成对象和代理类。 + +```java +// 方法 1: 该方法用于获取指定代理对象所关联的调用处理器 +public static InvocationHandler getInvocationHandler(Object proxy) + +// 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象 +public static Class getProxyClass(ClassLoader loader, +Class... interfaces) + +// 方法 3:该方法用于判断指定类对象是否是一个动态代理类 +public static boolean isProxyClass(Class cl) + +// 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例 +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces,InvocationHandler h) +``` + + +- `InvocationHandler`:调用处理器接口,自定义invoke方法,用于实现对于真正委托类的代理访问。 + +```java +/** + 该方法负责集中处理动态代理类上的所有方法调用。 + 第一个参数既是代理类实例, + 第二个参数是被调用的方法对象 + 第三个方法是调用参数。 + 调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行 +*/ +public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable; +``` + +- `ClassLoader`:类装载器类,将类的字节码装载到`Java`虚拟机`(JVM)`中并为其定义类对象,然后该类才能被使用。 +`Proxy`类与普通类的唯一区别就是其字节码是由`JVM`在运行时动态生成的而非预存在于任何一个`.class`文件中。 +每次生成动态代理类对象时都需要指定一个类装载器对象:`newProxyInstance()`方法第一个参数。 + + +动态代理机制 +--- + +`java`动态代理创建对象的过程为如下步骤: + +- 通过实现`InvocationHandler`接口创建自己的调用处理器 +```java +// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发 +// 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用 +InvocationHandler handler = new InvocationHandlerImpl(..); +``` + +- 通过为`Proxy`类指定`ClassLoader`对象和一组`interface`来创建动态代理类 +```java +// 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象 +Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... }); +``` +- 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型 +```java +// 通过反射从生成的类对象获得构造函数对象 +Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); +``` + +- 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入 +```java +// 通过构造函数对象创建动态代理类实例 +Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler }); +``` + +为了简化对象创建过程,`Proxy`类中的`newProxyInstance`方法封装了2~4,只需两步即可完成代理对象的创建。 +```java +// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发 +InvocationHandler handler = new InvocationHandlerImpl(..); + +// 通过 Proxy 直接创建动态代理类实例 +Interface proxy = (Interface)Proxy.newProxyInstance( classLoader, + new Class[] { Interface.class }, + handler ); +``` + + +动态代理的注意点 +--- + +- 包:代理接口是`public`,则代理类被定义在顶层包`(package为空)`,否则`(default)`,代理类被定义在该接口所在包, +- 生成的代理类为`public final`的,不能被继承 +- 类名:格式是`$ProxyN`,`N`是逐一递增的数字,代表`Proxy`被第`N`次动态生成的代理类,要注意对于同一组接口(接口的排列顺序也相同),不会重复创建动态代理类,而是返回一个先前已经创建并缓存了的代理类对象。提高了效率。 +- `Proxy`类是它的父类,这个规则适用于所有由`Proxy`创建的动态代理类。(也算是`java`动态代理的一处缺陷,`java`不支持多继承,所以无法实现对`class`的动态代理,只能对于`Interface`的代理)而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。 +- 代理类的根类`java.lang.Object`中有三个方法也同样会被分派到调用处理器的`invoke`方法执行,它们是`hashCode`,`equals`和`toString`. + + +示例代码: + +```java +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +public class HelloServiceProxy implements InvocationHandler { + + + private Object target; + /** + * 绑定委托对象并返回一个【代理占位】 + * @param target 真实对象 + * @return 代理对象【占位】 + */ + public Object bind(Object target, Class[] interfaces) { + this.target = target; + //取得代理对象 + return Proxy.newProxyInstance(target.getClass().getClassLoader(), + target.getClass().getInterfaces(), this); + } + + @Override + /** + * 同过代理对象调用方法首先进入这个方法. + * @param proxy --代理对象 + * @param method -- 方法,被调用方法. + * @param args -- 方法的参数 + */ + public Object invoke(Object proxy , Method method, Object[] args) throws Throwable { + System.err.println("############我是JDK动态代理################"); + Object result = null; + //反射方法前调用 + System.err.println("我准备说hello。"); + //反射执行方法 相当于调用target.sayHelllo; + result=method.invoke(target, args); + //反射方法后调用. + System.err.println("我说过hello了"); + return result; + } +} +``` + +其中,`bind`方法中的`newProxyInstance()`方法,就是生成一个代理对象,第一个参数是类加载器,第二个参数是真实委托对象所实现的的接口(代理对象挂在那个接口下),第三个参数`this`代表当前`HelloServiceProxy`类,换句话说是使用`HelloServiceProxy`作为对象的代理。 + +`invoke()`方法有三个参数:第一个`proxy`是代理对象,第二个是当前调用那个方法,第三个是方法的参数。 + +```java +public class ProxyTest { + + public static void main(String[] args) { + HelloServiceProxy proxy = new HelloServiceProxy(); + HelloService service = new HelloServiceImpl(); + //绑定代理对象。 + service = (HelloService) proxy.bind(service, new Class[] {HelloService.class}); + //这里service经过绑定,就会进入invoke方法里面了。 + service.sayHello("张三"); + } +} +``` + + +源码跟踪 +--- + +- `Proxy`类 + +```java +// 映射表:用于维护类装载器对象到其对应的代理类缓存 +private static Map loaderToCache = new WeakHashMap(); + +// 标记:用于标记一个动态代理类正在被创建中 +private static Object pendingGenerationMarker = new Object(); + +// 同步表:记录已经被创建的动态代理类类型,主要被方法 isProxyClass 进行相关的判断 +private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap()); + +// 关联的调用处理器引用 +protected InvocationHandler h; + +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h) + throws IllegalArgumentException { + + // 检查 h 不为空,否则抛异常 + if (h == null) { + throw new NullPointerException(); + } + + // 获得与制定类装载器和一组接口相关的代理类类型对象 + /* + * Look up or generate the designated proxy class. + */ + Class cl = getProxyClass0(loader, interfaces); + + // 通过反射获取构造函数对象并生成代理类实例 + /* + * Invoke its constructor with the designated invocation handler. + */ + try { + final Constructor cons = cl.getConstructor(constructorParams); + final InvocationHandler ih = h; + SecurityManager sm = System.getSecurityManager(); + if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) { + // create proxy instance with doPrivilege as the proxy class may + // implement non-public interfaces that requires a special permission + return AccessController.doPrivileged(new PrivilegedAction() { + public Object run() { + return newInstance(cons, ih); + } + }); + } else { + return newInstance(cons, ih); + } + } catch (NoSuchMethodException e) { + throw new InternalError(e.toString()); + } +} + +private static Object newInstance(Constructor cons, InvocationHandler h) { + try { + return cons.newInstance(new Object[] {h} ); + } catch (IllegalAccessException e) { + throw new InternalError(e.toString()); + } catch (InstantiationException e) { + throw new InternalError(e.toString()); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new InternalError(t.toString()); + } + } +} +``` + +动态代理真正的关键是在`getProxyClass()`方法,该方法主要分为四个步骤: + +- 对这组接口进行一定程度的安全检查 +检查接口类对象是否对类装载器可见并且与类装载器所能识别的接口类对象是完全相同的,还会检查确保是`interface`类型而不是`class`类型。 + +- 从`loaderToCache()`映射表中获取以类装载器对象为关键字所对应的缓存表,如果不存在就创建一个新的缓存表并更新到`loaderToCache()`。`loaderToCache()`存放键值对(接口名字列表,动态生成的代理类的类对象引用)。当代理类正在被创建时它会临时保存(接口名字列表`pendingGenerationMarker`)。标记`pendingGenerationMarke()`的作用是通知后续的同类请求(接口数组相同且组内接口排列顺序也相同)代理类正在被创建,请保持等待直至创建完成。 + +```java +/* + * Find or create the proxy class cache for the class loader. + */ +Map cache; +synchronized (loaderToCache) { + cache = (Map) loaderToCache.get(loader); + if (cache == null) { + cache = new HashMap(); + loaderToCache.put(loader, cache); + } + } +。。。。。 +do { + // 以接口名字列表作为关键字获得对应 cache 值 + Object value = cache.get(key); + if (value instanceof Reference) { + proxyClass = (Class) ((Reference) value).get(); + } + if (proxyClass != null) { + // 如果已经创建,直接返回 + return proxyClass; + } else if (value == pendingGenerationMarker) { + // 代理类正在被创建,保持等待 + try { + cache.wait(); + } catch (InterruptedException e) { + } + // 等待被唤醒,继续循环并通过二次检查以确保创建完成,否则重新等待 + continue; + } else { + // 标记代理类正在被创建 + cache.put(key, pendingGenerationMarker); + // break 跳出循环已进入创建过程 + break; +} while (true); +``` + +- 动态创建代理类的`class`对象 + +```java +/** + * Choose a name for the proxy class to generate. + */ +long num; +synchronized (nextUniqueNumberLock) { + num = nextUniqueNumber++; +} +String proxyName = proxyPkg + proxyClassNamePrefix + num; +/* + * Verify that the class loader hasn't already + * defined a class with the chosen name. + */ + +// 动态地生成代理类的字节码数组 +byte[] proxyClassFile = ProxyGenerator.generateProxyClass( + proxyName, interfaces); +try { +// 动态地定义新生成的代理类 + proxyClass = defineClass0(loader, proxyName, + proxyClassFile, 0, proxyClassFile.length); +} catch (ClassFormatError e) { + /* + * A ClassFormatError here means that (barring bugs in the + * proxy class generation code) there was some other + * invalid aspect of the arguments supplied to the proxy + * class creation (such as virtual machine limitations + * exceeded). + */ + throw new IllegalArgumentException(e.toString()); +} +// 把生成的代理类的类对象记录进 proxyClasses 表 +proxyClasses.put(proxyClass, null); +``` +首先根据规则(接口`public`与否),生成代理类的名称,`$ProxyN`格式,然后动态生成代理类。 +所有的代码生成的工作都由`ProxyGenerator`所完成了,该类在`rt.jar`中,需要反编译。 + +```java +public static byte[] generateProxyClass(final String name, + Class[] interfaces) { + ProxyGenerator gen = new ProxyGenerator(name, interfaces); +// 这里动态生成代理类的字节码,由于比较复杂就不进去看了 + final byte[] classFile = gen.generateClassFile(); + +// 如果saveGeneratedFiles的值为true,则会把所生成的代理类的字节码保存到硬盘上 + if (saveGeneratedFiles) { + java.security.AccessController.doPrivileged( + new java.security.PrivilegedAction() { + public Void run() { + try { + FileOutputStream file = + new FileOutputStream(dotToSlash(name) + ".class"); + file.write(classFile); + file.close(); + return null; + } catch (IOException e) { + throw new InternalError( + "I/O exception saving generated file: " + e); + } + } + }); + } + +// 返回代理类的字节码 + return classFile; +} +``` + +- 代码生成过程进入结尾部分,根据结果更新缓存表,如果成功则将代理类的类对象引用更新进缓存表,否则清楚缓存表中对应关键值,最后唤醒所有可能的正在等待的线程。 + +```java +finally { + /* + * We must clean up the "pending generation" state of the proxy + * class cache entry somehow. If a proxy class was successfully + * generated, store it in the cache (with a weak reference); + * otherwise, remove the reserved entry. In all cases, notify + * all waiters on reserved entries in this cache. + */ + synchronized (cache) { + if (proxyClass != null) { + cache.put(key, new WeakReference(proxyClass)); + } else { + cache.remove(key); + } + cache.notifyAll(); + } +} +return proxyClass; +``` + +`JDK`是动态生成代理类,并通过调用解析器,执行接口实现的方法的原理已经一目了然。动态代理加上反射,是很多框架的基础。 +比如`Spring`的`AOP`机制,自定义前置后置通知等控制策略,以及`mybatis`中的运用反射和动态代理来实现插件技术等等。 + + +- [Proxy源码](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/reflect/Proxy.java) + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Java\345\237\272\347\241\200/\345\215\225\344\276\213\347\232\204\346\234\200\344\275\263\345\256\236\347\216\260\346\226\271\345\274\217.md" "b/JavaKnowledge/\345\215\225\344\276\213\347\232\204\346\234\200\344\275\263\345\256\236\347\216\260\346\226\271\345\274\217.md" similarity index 95% rename from "Java\345\237\272\347\241\200/\345\215\225\344\276\213\347\232\204\346\234\200\344\275\263\345\256\236\347\216\260\346\226\271\345\274\217.md" rename to "JavaKnowledge/\345\215\225\344\276\213\347\232\204\346\234\200\344\275\263\345\256\236\347\216\260\346\226\271\345\274\217.md" index 02c2a2bb..53743211 100644 --- "a/Java\345\237\272\347\241\200/\345\215\225\344\276\213\347\232\204\346\234\200\344\275\263\345\256\236\347\216\260\346\226\271\345\274\217.md" +++ "b/JavaKnowledge/\345\215\225\344\276\213\347\232\204\346\234\200\344\275\263\345\256\236\347\216\260\346\226\271\345\274\217.md" @@ -1,26 +1,26 @@ -单例的最佳实现方式 -=== - -```java -public class Singleton { - // Private constructor prevents instantiation from other classes - private Singleton() { } - - /** - * SingletonHolder is loaded on the first execution of Singleton.getInstance() - * or the first access to SingletonHolder.INSTANCE, not before. - */ - private static class SingletonHolder { - public static final Singleton INSTANCE = new Singleton(); - } - - public static Singleton getInstance() { - return SingletonHolder.INSTANCE; - } -} -``` - ---- - -- 邮箱 :charon.chui@gmail.com +单例的最佳实现方式 +=== + +```java +public class Singleton { + // Private constructor prevents instantiation from other classes + private Singleton() { } + + /** + * SingletonHolder is loaded on the first execution of Singleton.getInstance() + * or the first access to SingletonHolder.INSTANCE, not before. + */ + private static class SingletonHolder { + public static final Singleton INSTANCE = new Singleton(); + } + + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } +} +``` + +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" new file mode 100644 index 00000000..8609744e --- /dev/null +++ "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" @@ -0,0 +1,596 @@ +常用命令行大全 +=== + + +文件目录操作命令 +--- + +- `ls` + + `ls`命令是`linux`下最常用的命令。`ls`命令就是`list`的缩写,默认情况下使用`ls`用来打印出当前目录的列表,如果`ls`指定其他目录, + 那么就会显示指定目录里的文件及文件夹列表。 通过`ls`命令不仅可以查看`linux`文件夹包含的文件,而且可以查看文件权限(包括目录、文件夹、文件权限), + 查看目录信息等等。`ls`命令在日常的`linux`操作中使用得比较多了。 + + 命令格式: + `ls [选项] [目录名]` + + 命令功能: + 列出目标目录中所有的子目录和文件。 + + 常用参: + + - `-a`:`–all`列出目录下的所有文件,包括以`.`开头的隐含文件。 + - `-A`同`-a`,但不列出“.”(表示当前目录)和“..”(表示当前目录的父目录)。 + - `-c` 配合 `-lt`:根据`ctime`排序及显示`ctime`(文件状态最后更改的时间)配合`-l:`显示`ctime`但根据名称排序否则:根据`ctime`排序 + - `-l` 除了文件名之外,还将文件的权限、所有者、文件大小等信息详细列出来。 + - `-s`,`–size`以块大小为单位列出所有文件的大小 + - `-S`根据文件大小排序 + - `-t` 以文件修改时间排序 + - `-X` 根据扩展名排序 + - `-1` 每行只列出一个文件 + - `–help` 显示此帮助信息并离开 + - `–version` 显示版本信息并离开 + + 列出`/home/yiibai`文件夹下的所有文件和目录的详细资料,命令: + ``` + ls -l -R /home/yiibai + ``` + + 列出当前目录中所有以`t`开头的目录的详细内容,可以使用如下命令: + ``` + ls -l t* + ``` + +- `cd` + + 格式: + `cd [目录名]` + + 切换当前目录至`dirName` + + 进入系统根目录`cd /` + + 使用`cd`命令进入当前用户主目录 + “当前用户主目录”和“系统根目录”是两个不同的概念。进入当前用户主目录有两个方法。 + ``` + cd ~ + ``` + +- `pwd` + + 查看”当前工作目录“的完整路径 + + + - 创建文件夹 + + `mkdir test` + + 递归创建多个目录或一次创建多级目录: + ``` + mkdir -p dir01/dir001 + ``` + + 创建权限为777的目录: + ``` + mkdir -m 777 test3 + ``` + +- 删除文件 + + 该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除。 + `rm xxx.txt` + `rm -rf file// -fr表示递归和强制,一定要小心使用 rm -fr / 那你的电脑就over了` + + - `-f`,`--force`忽略不存在的文件,从不给出提示。 + - `-i`,`--interactive`进行交互式删除 + - `-r`,`-R`,`--recursive`指示`rm`将参数中列出的全部目录和子目录均递归地删除。 + +- `rmdir` + + 该命令的功能是删除空目录,一个目录被删除之前必须是空的。删除某目录时也必须具有对父目录的写权限。由于只能删除空目录,一般都是使用rm -f + +- `mv` + + `mv`命令是`move`的缩写,可以用来移动文件或者将文件改名`(move (rename) files)`,是`Linux`系统下常用的命令,经常用来备份文件或者目录。 + + ``` + mv [选项] 源文件或目录 目标文件或目录 + ``` + + `mv`命令中第二个参数类型的不同(是目标文件还是目标目录),`mv`命令将文件重命名或将其移至一个新的目录中。 + 当第二个参数类型是文件时,`mv`命令完成文件重命名,此时,源文件只能有一个(也可以是源目录名), + 它将所给的源文件或目录重命名为给定的目标文件名。当第二个参数是已存在的目录名称时, + 源文件或目录参数可以有多个,`mv`命令将各参数指定的源文件均移至目标目录中。在跨文件系统移动文件时, + `mv`先拷贝,再将原有文件删除,而链至该文件的链接也将丢失。 + + 文件改名: + ``` + mv test.log new-test.log + ``` + + 移动文件: + ``` + mv test1.txt test3 + ``` + +- `touch` + + `linux`中的`touch`命令不常用,一般用来修改文件时间戳,或者新建一个不存在的文件。 + + 创建不存在的文件,用法如下所示: + ``` + touch log1.log log2.log + ``` + + +- `cat` + + 查看文本内容:`cat xxx.txt` + + `cat`主要有三大功能: + + - 一次显示整个文件`cat filename` + - 从键盘创建一个文件`cat > filename`只能创建新文件,不能编辑已有文件. + - 将几个文件合并为一个文件`cat file1 file2 > file` + 命令参数: + + - `-A`,`--show-all`等价于`-vET` + - `-b`,`--number-nonblank`对非空输出行编号 + - `-e`等价于`-vE` + - `-E`,`--show-ends`在每行结束处显示`$` + - `-n`,`--number`对输出的所有行编号,由1开始对所有输出的行数编号 + - `-s`,`--squeeze-blank`有连续两行以上的空白行,就代换为一行的空白行 + - `-t`与`-vT`等价 + - `-T`,`--show-tabs`将跳格字符显示为`^I` + + - 拷贝 + `cp -R 源文件 目标文件// -R 表示对目录进行递归操作` + + +- `cp` + + `cp`命令用来复制文件或者目录,是`Linux`系统中最常用的命令之一。 + + 用法: + ``` + cp [选项]... [-T] 源 目的 + 或: + cp [选项]... 源... 目录 + 或: + cp [选项]... -t 目录 源... + ``` + + 复制单个文件到目标目录,文件在目标文件中不存在,如果目标文件存在时,会询问是否覆盖: + ``` + cp log.log logs + ``` + + +- `tar` + + 在维护配置服务器时,难免会要用到压缩,解压缩,打包,解包等,这时候`tar`命令就是是必不可少的一个功能强大的工具。 + `linux`中最流行的`tar`是麻雀虽小,五脏俱全,功能强大。 + + 首先要弄清两个概念:打包和压缩。打包是指将一大堆文件或目录变成一个总的文件;压缩则是将一个大的文件通过一些压缩算法变成一个小文件。 + + 为什么要区分这两个概念呢?这源于`Linux`中很多压缩程序只能针对一个文件进行压缩, + 这样当你想要压缩一大堆文件时,你得先将这一大堆文件先打成一个包(`tar`命令),然后再用压缩程序进行压缩(`gzip, bzip2`命令)。 + + `linux`下最常用的打包程序就是`tar`了,使用`tar`程序打出来的包我们常称为`tar`包,`tar`包文件的命令通常都是以`.tar`结尾的。 + 生成`tar`包后,就可以用其它的程序来进行压缩。 + + 格式: + ``` + tar[必要参数][选择参数][文件] + ``` + 命令功能; + + 用来压缩和解压文件。tar本身不具有压缩功能,它是调用压缩功能实现的。 + + 必要参数有如下: + + - `-A`:新增压缩文件到已存在的压缩 + - `-B`:设置区块大小 + - `-c`:建立新的压缩文件 + - `-d`:记录文件的差别 + - `-r`:添加文件到已经压缩的文件 + - `-u`:添加改变了和现有的文件到已经存在的压缩文件 + - `-x`:从压缩的文件中提取文件 + - `-t`:显示压缩文件的内容 + - `-z`:支持gzip解压文件 + - `-j`:支持bzip2解压文件 + - `-k`:保留原有文件不覆盖 + - `-m`:保留文件不被覆盖 + + 可选参数如下: + + - `-b`:设置区块数目 + - `-C`:切换到指定目录 + - `-f`:指定压缩文件 + - `--help`:显示帮助信息 + - `--version`:显示版本信息 + + +- 常见解压/压缩命令 + + `tar`文件格式: + ``` + 解包:tar xvf FileName.tar + 打包:tar cvf FileName.tar DirName + (注:tar是打包,不是压缩!) + ``` + + `.gz`文件格式: + + ``` + 解压1:gunzip FileName.gz + 解压2:gzip -d FileName.gz + 压缩:gzip FileName + ``` + + `.tar.gz`和`.tgz`: + ``` + 解压:tar zxvf FileName.tar.gz + 压缩:tar zcvf FileName.tar.gz DirName + ``` + + `.bz2`文件格式: + ``` + 解压1:bzip2 -d FileName.bz2 + 解压2:bunzip2 FileName.bz2 + 压缩: bzip2 -z FileName + ``` + + `.tar.bz2`文件格式: + ``` + 解压:tar jxvf FileName.tar.bz2 + 压缩:tar jcvf FileName.tar.bz2 DirName + ``` + + `.bz`文件格式: + ``` + 解压1:bzip2 -d FileName.bz + 解压2:bunzip2 FileName.bz + 压缩:未知 + ``` + + `.tar.bz`文件格式: + ``` + 解压:tar jxvf FileName.tar.bz + 压缩:未知 + ``` + + `.Z`文件格式: + ``` + 解压:uncompress FileName.Z + 压缩:compress FileName + ``` + + `.tar.Z`文件格式: + ``` + 解压:tar Zxvf FileName.tar.Z + 压缩:tar Zcvf FileName.tar.Z DirName + ``` + + `.zip`文件格式: + ``` + 解压:unzip FileName.zip + 压缩:zip FileName.zip DirName + ``` + + `.rar`: + ``` + 解压:rar x FileName.rar + 压缩:rar a FileName.rar DirName + ``` + + 将文件全部打包成`tar`包: + ``` + tar -cvf log1.tar log1.log + tar -zcvf log2.tar.gz log2.log + tar -jcvf log3.tar.bz2 log3.log + ``` + + ``` + tar -cvf log1.tar log1.log 仅打包,不压缩! + tar -zcvf log2.tar.gz log2.log 打包后,以 gzip 压缩 + tar -zcvf log.tar.bz2 log3.log 打包后,以 bzip2 压缩 + 在参数 f 之后的文件档名是自己取的,我们习惯上都用 .tar 来作为辨识。 如果加 z 参数,则以 .tar.gz 或 .tgz 来代表 gzip 压缩过的 tar 包; 如果加 j 参数,则以 .tar.bz2 来作为tar包名。 + ``` + + +- `gzip` + + `gzip`是在`Linux`系统中经常使用的一个对文件进行压缩和解压缩的命令,既方便又好用。 + `gzip`不仅可以用来压缩大的、较少使用的文件以节省磁盘空间,还可以和`tar`命令一起构成`Linux`操作系统中比较流行的压缩文件格式。 + 据统计,`gzip`命令对文本文件有60%~70%的压缩率。 + + 格式: + ``` + gzip[参数][文件或者目录] + ``` + + 功能: + `gzip`是个使用广泛的压缩程序,文件经它压缩过后,其名称后面会多出`.gz`的扩展名。 + + 命令参数: + + - `-a`或`--ascii`使用`ASCII`文字模式。 + - `-c`或`--stdout`或`--to-stdout`:把压缩后的文件输出到标准输出设备,不去更动原始文件。 + - `-d`或`--decompress`或`----uncompress`:解开压缩文件。 + - `-f`或`--force`:强行压缩文件。不理会文件名称或硬连接是否存在以及该文件是否为符号连接。 + - `-N`或`--name`:压缩文件时,保存原来的文件名称及时间戳记。 + - `-r`或`--recursive`:递归处理,将指定目录下的所有文件及子目录一并处理。 + + 把目录下的每个文件压缩成`.gz`文件: + + ``` + gzip * + ``` + + 把例1中每个压缩的文件解压,并列出详细的信息: + ``` + gzip -dv * + ``` + + +文件查找 +--- + +- `whereis` + + `whereis`命令只能用于程序名的搜索,而且只搜索二进制文件(参数`-b`)、`man`说明文件(参数`-m`)和源代码文件(参数`-s`)。 + 如果省略参数,则返回所有信息。和`find`相比,`whereis`查找的速度非常快,这是因为`linux`系统会将系统内的所有文件都记录在一个数据库文件中, + 当使用`whereis`和下面即将介绍的locate时,会从数据库中查找数据,而不是像find命令那样,通 过遍历硬盘来查找,效率自然会很高。 + + 格式: + ``` + whereis [-bmsu] [BMS 目录名 -f ] 文件名 + ``` + + 将和xx文件相关的文件都查找出来: + ``` + whereis python + ``` + + +- `find` + + `Linux`下`find`命令提供了相当多的查找条件,功能很强大。 + + 格式: + ``` + find pathname -options [-print -exec -ok ...] + ``` + + 功能: + + 用于在文件树种查找文件,并作出相应的处理 + + 命令参数: + + - `pathname`:`find`命令所查找的目录路径。例如用.来表示当前目录,用/来表示系统根目录。 + + 命令选项: + + - `-name`按照文件名查找文件。 + - `-perm`按照文件权限来查找文件。。 + - `-mtime`:`-n +n`按照文件的更改时间来查找文件,`- n`表示文件更改时间距现在`n`天以内,+ n表示文件更改时间距现在n天以前 + - `-type`:查找某一类型的文件,诸如: + + - `b` - 块设备文件。 + - `d` - 目录。 + - `c` - 字符设备文件。 + - `p` - 管道文件。 + - `l` - 符号链接文件。 + - `f` - 普通文件。 + + 根据关键字查找: + ``` + find . -name "*.log" + ``` + + +#### 使用`-name`选项 + +文件名选项是`find`命令最常用的选项,要么单独使用该选项,要么和其他选项一起使用。 +可以使用某种文件名模式来匹配文件,记住要用引号将文件名模式引起来。不管当前路径是什么, +如果想要在自己的根目录$HOME中查找文件名符合`*.log`的文件,使用~作为`pathname`参数,波浪号`~`代表了当前用户的`$HOME`目录。 +``` +find ~ -name "*.log" -print +``` +想要在当前目录及子目录中查找所有的‘ *.log‘文件,可以用: +``` +find . -name "*.log" -print +``` +想要的当前目录及子目录中查找文件名以一个大写字母开头的文件,可以用: +``` +find . -name "[A-Z]*" -print +``` +想要在/etc目录中查找文件名以host开头的文件,可以用: +``` +find /etc -name "host*" -print +``` +想要查找`$HOME`目录中的文件,可以用: +``` +find ~ -name "*" -print 或find . -print +``` + +权限设置 +--- + + +- `chmod` + +命令格式: +``` +chmod [-cfvR] [—help] [—version] mode file +``` + +命令功能: +用于改变文件或目录的访问权限,用它控制文件或目录的访问权限。 + +必要参数: + +- `-c`:当发生改变时,报告处理信息 +- `-f`:错误信息不输出 +- `-R`:处理指定目录以及其子目录下的所有文件 + +权限代号: + +- `r`:读权限,用数字4表示 +- `w`:写权限,用数字2表示 +- `x`:执行权限,用数字1表示 +- `-`:删除权限,用数字0表示 +- `s`:特殊权限 +- + +文字设定法: +``` +chmod [who] [+ | - | =] [mode] 文件名 +``` + +我们必须首先了解用数字表示的属性的含义:0表示没有权限,1表示可执行权限,2表示可写权限,4表示可读权限,然后将其相加。所以数字属性的格式应为3个从0到7的八进制数,其顺序是(u)(g)(o)。 +例如,如果想让某个文件的属主有“读/写”二种权限,需要把4(可读)+2(可写)=6(读/写)。 +数字设定法的一般形式为: +`chmod [mode] 文件名` + +数字与字符对应关系如下: + +`r=4,w=2,x=1` +- 若要`rwx`属性则4+2+1=7 +- 若要`rw-`属性则4+2=6; +- 若要`r-x`属性则4+1=7。 + + +如: +``` +chmod 751 file +``` +如果是当前root用户执行,前面需要加 sudo chmod 751 file + + +性能监控和优化命令 +--- + +- `top` + + `top`命令是`Linux`下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于`Windows`的任务管理器。 + +格式: + ``` +top [参数] + ``` + +- `ifconfig` + +许多`windows`非常熟悉`ipconfig`命令行工具,它被用来获取网络接口配置信息并对此进行修改。`Linux`系统拥有一个类似的工具, +也就是`ifconfig`(`interfaces config`)。通常需要以`root`身份登录或使用`sudo`以便在`Linux`机器上使用`ifconfig`工具。 + + +格式: +``` +ifconfig [网络设备] [参数] +``` + +- `ping` + +`linux`下的`ping`命令和`windows`下的`ping`执行稍有区别,`linux`下`ping`不会自动终止,需要按`ctrl+c`终止或者用参数`-c`指定要求完成的回应次数。 + + + +- 清除命令行内容 + `clear` + +- 显示当前日期 + `date` + +- 关机 + `sudo shutdown -h now`// -h 是关闭电源 now立即关机 + `sudo shutdown -r now`//重启 + `sudo shutdown -h -time 60`// 表示60分钟后关机,注意单位是分钟 + + + +- file命令是一个方便的小工具,能够探测文件的内部并判断文件类型: +$ file .bashrc +.bashrc: ASCII text + + +- pkill命令可以使用程序名代替PID来终止进程,这就方便多了。除此之外,pkill命令也允许使用通配符,当系统出问题时,这是一个非常有用的工具: +# pkill http* +# +该命令将“杀死”所有名称以http起始的进程,比如Apahce Web Server的httpd服务。 +警告 以root身份使用pkill命令时要格外小心。命令中的通配符很容易意外地将系统的重要进程终止。这可能会导致文件系统损坏。 + + +### Shell脚本 + +Shell的作用就是解释执行用户的命令,用户输入一条命令,Shell就解释执行一条,这种方式称为交互式,Shell还有一种执行命令的方式称为批处理,用户事先写一个Shell脚本,其中有很多条命令,让Shell一次把这些命令执行完,而不是一条一条的敲命令。 +Shell脚本和编程语言很类似,也有变量和流程控制语句,但Shell脚本是解释执行的,不需要编译,Shell程序从脚本中一行一行读取并执行这些命令,相当于一个用户把脚本中的命令一行一行敲到Shell提示符下执行。 + + +```shell +#!/bin/bash +# This line is a comment +echo "Hello World" +``` +一个shell脚本永远以#!开头,这个是一个脚本开始的标记,它是告诉系统执行这个文件需要使用某个解释器,后面的/bin/bash指明了解释器的具体位置。 +第二行 # 这里表示是一个注解 +第三行是输出Hello World + +#### 运行脚本 +脚本的运行有好几种方式: +- 在该脚本所在的目录中直接bash这个脚本,直接bash一个文件就是指定了使用Bash Shell来解释脚本内容。 +- 给该脚本加上可执行权限,然后使用./来运行,它代表运行的是当前目录下的Hello World.sh脚本, 如果采用这种方式而脚本没有可执行权限则会报错。 + ```shell + sudu chmod 777 HelloWorld.sh + ./HelloWorld.sh + ``` +- 如果不想修改权限可以使用.来运行脚本 + ```shell + . ./HelloWorld.sh + ``` +###### 声明变量: declare、typeset +这两个命令都是用来声明变量的,作用完全相同。 +很多语法严谨的语言对变量的声明都是有严格要求的,变量的使用原则是必须在使用前声明、声明时必须说明变量类型,而shell脚本中对变量声明的要求并不高,因为shell弱化了变量的类概念,所有shell又称为弱类型编程语言,声明变量时并不需要指明类型。 + + +###### 函数 +```shell +function NAME() { + ..... + return x; +} +``` +function关键字可以省略。 + +###### 命令代换 + +```shell +DATE=`date` +echo $DATE +``` +由“`”反引号括起来的也是一条命令,Shell先执行该命令,然后将输出结果立刻代换到当前命令行中,也可以用$()表示: +```shell +DATE = $(date) +``` + + +####### 输入输出 +echo: 显示文本行或变量,或者把字符串输入到文件 + +###### tee +tee 命令把结果输出到标准输出,另一个副本输出到相应文件中。 + + + + + + + +---- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" "b/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" new file mode 100644 index 00000000..c656ea7f --- /dev/null +++ "b/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" @@ -0,0 +1,87 @@ +强引用、软引用、弱引用、虚引用 +=== + +在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这就像在日常生活中,从商店购买了某样物品后,如果有用,就一直保留它,否则就把它扔到垃圾箱,由清洁工人收走。一般说来,如果物品已经被扔到垃圾箱,想再把它捡回来使用就不可能了。 +但有时候情况并不这么简单,你可能会遇到类似鸡肋一样的物品,食之无味,弃之可惜。这种物品现在已经无用了,保留它会占空间,但是立刻扔掉它也不划算,因为也许将来还会派用场。对于这样的可有可无的物品,一种折衷的处理办法是:如果家里空间足够,就先把它保留在家里,如果家里空间不够,即使把家里所有的垃圾清除,还是无法容纳那些必不可少的生活用品,那么再扔掉这些可有可无的物品。 +从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。 + +***这四种级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用。*** + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/reference_list.jpg) + +在java.lang.ref包中提供了三个类:SoftReference类、WeakReference类和PhantomReference类,它们分别代表软引用、弱引用和虚引用。ReferenceQueue类表示引用队列,它可以和这三种引用类联合使用,以便跟踪Java虚拟机回收所引用的对 象的活动。 + + + +- 强引用(Strong Reference) + + 你懂的,不要胡乱持有着不放,不然内存泄露、oom有你好看,就像是老板(OOM)的亲儿子一样,在公司可以什么事都不干,但是千万不要老是占用公司的资源为他自己做事,记得用完公司的妹子之后,要让她们去工作(资源要懂得释放) 不然公司很可能会垮掉的。 + + 平时我们编程的时候例如:`Object object=new Object()`;那`object`就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。 + +- 软引用(SoftReference) + + 描述一些还有用,但并非必需的对象。如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存,但是system.gc对其无效,有点像老板(OOM)的亲戚,在公司表现不好有可能会被开除,即使你投诉他(调用GC)上班看片,但是只要不被老板看到(被JVM检测到)就不会被开除(被虚拟机回收)。**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 + +- 弱引用(WeakReference) + + 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。所以弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的声明周期。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 + + 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。 + + 常见的一个例子就是WeakHashMap,在HashMap中,键被置为null,唤醒gc后,不会垃圾回收键为null的键值对。但是在WeakHashMap中,键被置为null,唤醒gc后,键为null的键值对会被回收。 + + ``` + public static void weakHashMapTest() { + Integer key = new Integer(1); + String value = "李四"; + Map weakHashMap = new WeakHashMap(); + weakHashMap.put(key, value); + System.out.println(weakHashMap);//{1=李四} + key = null; + System.gc(); + System.out.println(weakHashMap);//{} + } + public static void hashMapTest() { + HashMap map = new HashMap<>(); + Integer key = 1; + String value = "张三"; + map.put(key,value); + System.out.println(map);//{1=张三} + key = null; + System.gc(); + System.out.println(map);//{1=张三} + } + ``` + +- 虚引用(PhantomReference) + + "虚引用"顾名思义,就是形同虚设,也成为幽灵引用或幻影引用,它是最弱的一种引用关系。就只是一个标识,对象的生命周期不受期影响,这货估计就是个临时工把,遇到事情的时候想到了你,没有事情的时候,秒秒钟拿出去顶锅,开除。 + + 一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一的用处:能在对象被GC时收到系统通知,主要用于跟踪对象何时被回收,比如防止资源泄漏等。 + + 虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/reference_compare.jpg) + + +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。在JDK 1.2版之前,Java里面的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。 + + +在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。 + + +- 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 + +- 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。 + +- 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。 + +- 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Java\345\237\272\347\241\200/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" "b/JavaKnowledge/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" similarity index 62% rename from "Java\345\237\272\347\241\200/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" rename to "JavaKnowledge/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" index 06560140..fa29a7b1 100644 --- "a/Java\345\237\272\347\241\200/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\345\212\240\345\257\206\345\217\212\350\247\243\345\257\206.md" @@ -21,19 +21,17 @@ - DES -`DES`全称为`Data Encryption Standard`,即数据加密标准,是一种使用密钥加密的块算法, -1976年被美国联邦政府的国家标准局确定为联邦资料处理标准(`FIPS`),随后在国际上广泛流传开来。 - -`DES`算法的入口参数有三个:`Key`、`Data`、`Mode`。其中`Key`为8个字节共64位,是`DES`算法的工作密钥;`Data`也为8个字节64位, -是要被加密或被解密的数据;`Mode`为`DES`的工作方式,有两种:加密或解密。 -`DES`算法把64位的明文输入块变为64位的密文输出块,它所使用的密钥也是64位。 - + DES`全称为`Data Encryption Standard`,即数据加密标准,是一种使用密钥加密的块算法, + 1976年被美国联邦政府的国家标准局确定为联邦资料处理标准(`FIPS`),随后在国际上广泛流传开来。` + `DES`算法的入口参数有三个:`Key`、`Data`、`Mode`。其中`Key`为8个字节共64位,是`DES`算法的工作密钥;`Data`也为8个字节64位, + 是要被加密或被解密的数据;`Mode`为`DES`的工作方式,有两种:加密或解密。 + `DES`算法把64位的明文输入块变为64位的密文输出块,它所使用的密钥也是64位。 - AES -`AES`全程为`Advanced Encryption Standard`,即高级加密标准,在密码学中又称`Rijndael`加密法, -是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的`DES`,已经被多方分析且广为全世界所使用。 -`AES`加密数据块分组长度必须为128比特,密钥长度可以是128比特、192比特、256比特中的任意一个(如果数据块及密钥长度不足时,会补齐) + `AES`全程为`Advanced Encryption Standard`,即高级加密标准,在密码学中又称`Rijndael`加密法, + 是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的`DES`,已经被多方分析且广为全世界所使用。 + `AES`加密数据块分组长度必须为128比特,密钥长度可以是128比特、192比特、256比特中的任意一个(如果数据块及密钥长度不足时,会补齐) @@ -55,10 +53,10 @@ 总结 --- - - 对称加密加密与解密使用的是同样的密钥,所以速度快,但由于需要将密钥在网络传输,所以安全性不高。 - - 非对称加密使用了一对密钥,公钥与私钥,所以安全性高,但加密与解密速度慢。 - - 解决的办法是将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥, - 然后双方可以使用对称加密来进行沟通。 +- 对称加密加密与解密使用的是同样的密钥,所以速度快,但由于需要将密钥在网络传输,所以安全性不高。 +- 非对称加密使用了一对密钥,公钥与私钥,所以安全性高,但加密与解密速度慢。 +- 解决的办法是将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥, +然后双方可以使用对称加密来进行沟通。 ---- - 邮箱 :charon.chui@gmail.com diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..b5d71dba --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,172 @@ +1. LeetCode_两数之和 +=== + +给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值target的那两个整数,并返回它们的数组下标。 + +你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 + +你可以按任意顺序返回答案。 + + +--- + +示例 1 + +输入:nums = [2,7,11,15], target = 9 +输出:[0,1] +解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 + + +示例 2: + +输入:nums = [3,2,4], target = 6 +输出:[1,2] + +示例 3: + +输入:nums = [3,3], target = 6 +输出:[0,1] + + + +提示: + +只会存在一个有效答案 + + +进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗? + + +### 方法一:暴力枚举 + + +最简单的方法就是对数组中的每一个数x,都去遍历找后面是否存在target - x的值是否存在。 + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + int size = nums.length; + + for (int i = 0; i < size; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) { + return new int[]{i, j}; + } + } + } + return new int[0]; + } +} +``` + +```c++ +#include +using namespace std; + + +class Solution { +public: + vector twoSum(vector &nums, int target) { + int size = nums.size(); + + for (int i = 0; i < size; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) { + return {i, j}; + } + } + } + return {}; + } +}; +``` + +复杂度分析: + +- 时间复杂度:O(N²),其中N是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。 + +- 空间复杂度:O(1)。 + + +### 方法二: 哈希表 + +注意到方法一的时间复杂度较高的原因是寻找target - x的时间复杂度过高。 + +因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。 + +使用哈希表,可以将寻找target - x的时间复杂度降低到从O(N)降低到O(1)。 + + +--- + +如果想要快速确定某个元素是否在nums数组中,并且可以快速的获取所在下表index。 +我们的第一反应就是将数组维护成一个Map结构: + +- key: 存储数组里的值 +- value: 存储数组的下标index + +这样我们只需要通过target - nums[i]的值去Map中查找即可。 +但是这样存在一个问题,就是你需要先把数组中的值都放到Map中,需要多一步循环。 + + +--- + +其实我们可以先创建一个Map,在Map的初始化过程中什么元素都不放。 + +对于每一个x,我们首先查询哈希表中是否存在target-x,如果已存在就返回,如果不存在那再将x插入到哈希表中,即可保证不会让x和自己匹配。 + +这样只需要一次循环就可以了,而且Map数组不用提前初始化,在性能和内存占用率都比较低。 + + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + Map hashtable = new HashMap(); + for (int i = 0; i < nums.length; ++i) { + if (hashtable.containsKey(target - nums[i])) { + return new int[]{hashtable.get(target - nums[i]), i}; + } + hashtable.put(nums[i], i); + } + return new int[0]; + } +} + +``` + +```c++ +#include +#include + +using namespace std; + +class Solution { +public: + vector twoSum(vector &nums, int target) { + unordered_map map; + for (int i = 0; i < nums.size(); i ++) { + auto it = map.find(target - nums[i]); + if (it != map.end()) { + return {i, it->second}; + } + + map[nums[i]] = i; + } + return {}; + } +}; +``` + + +复杂度分析: + +- 时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。 + +- 空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" new file mode 100644 index 00000000..03581f19 --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" @@ -0,0 +1,210 @@ +2. LeetCode_两数相加 +=== + +给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。 + +请你将两个数相加,并以相同形式返回一个表示和的链表。 + +你可以假设除了数字0之外,这两个数都不会以0开头。 + + +输入:l1 = [2,4,3], l2 = [5,6,4] +输出:[7,0,8] +解释:342 + 465 = 807. + + + +输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] +输出:[8,9,9,9,0,0,0,1] + + +提示: + +- 每个链表中的节点数在范围[1, 100]内 +- 0 <= Node.val <= 9 +- 题目数据保证列表表示的数字不含前导零 + + + +想法: + +我的想法是先把两个链表转成两个整数相加,然后把这个整数转成字符串再次生成链表。 + +这种方案是不可行的,因为链表的长度可能会很长,整数是操作不了的,例如: + + +方法: + +- 由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。 + +- 我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry,则它们的和为 n1+n2+carry; +其中,答案链表处相应位置的数字为 (n1+n2+carry)mod10,而新的进位值为 [(n1+n2+carry) / 10] + +- 如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0 。 + +- 此外,如果链表遍历结束后,有carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。 + + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_2_twoaddsum.png?raw=true) + + +- 将链表反过来看,头结点在右侧 +- 横线上的数字为进位 +- 2 + 5 + 0(第一个进位默认为0) = 7 + + - 7 % 10得到新节点中的元素为7 + - 7 / 10得到下一个进位为0 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_2_twoaddsum_2.png?raw=true) + + +```java +class ListNode { + int val; + ListNode next; + + ListNode() { + this.val = 0; + this.next = null; + } + + ListNode(int val) { + this.val = val; + this.next = null; + } + + ListNode(int val, ListNode next) { + this.val = val; + this.next = next; + } +} + +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + ListNode head = null; + ListNode tail = null; + + int carry = 0; + + while(l1 != null || l2 != null) { + int n1 = l1 != null ? l1.val : 0; + int n2 = l2 != null ? l2.val : 0; + + int sum = n1 + n2 + carry; + + if (head == null) { + head = tail = new ListNode(sum % 10); + } else { + tail.next = new ListNode(sum % 10); + tail = tail.next; + } + + carry = sum / 10; + + if (l1 != null) { + l1 = l1.next; + } + + if (l2 != null) { + l2 = l2.next; + } + } + + if (carry > 0) { + tail.next = new ListNode(carry); + } + return head; + } +} +``` + + + +复杂度分析: + +- 时间复杂度: O(max(m,n)),其中m和n分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要O(1)的时间。 + +- 空间复杂度: O(1)。注意返回值不计入空间复杂度。 + + +### 改进: 递归 + +```java +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + return add(l1, l2, 0); + } +} + +public ListNode add(ListNode l1, ListNode l2, int carry) { + if (l1 == null && l2 == null && carry == 0) { + return null; + } + int val = carry; + if (l1 != null) { + val += l1.val; + l1 = l1.next; + } + if (l2 != null) { + val += l2.val; + l2 = l2.next; + } + ListNode node = new ListNode(val % 10); + node.next = add(l1, l2, val / 10); + return node; + } +} +``` + +使用C++实现: + +```c++ +struct ListNode { + int val; + ListNode *next; + + ListNode() : val(0), next(nullptr) { + } + + ListNode(int x) : val(x), next(nullptr) { + } + + ListNode(int x, ListNode *next) : val(x), next(next) { + } +}; + +class Solution { +public: + ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) { + int carry = 0; + + ListNode *answer = new ListNode(); + ListNode *head = answer; + + while (l1 || l2) { + int number = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry; + carry = number / 10; + number %= 10; + head->next = new ListNode(number); + head = head->next; + + l1 ? l1 = l1->next : 0; + l2 ? l2 = l2->next : 0; + } + if (carry) { + head->next = new ListNode(carry); + } + return answer->next; + } +}; +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" new file mode 100644 index 00000000..748e420e --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" @@ -0,0 +1,190 @@ +3. LeetCode_无重复字符的最长子串 +=== + +给定一个字符串s,请你找出其中不含有重复字符的 最长子串 的长度。 + + +示例 1: + +输入: s = "abcabcbb" +输出: 3 +解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 +示例 2: + +输入: s = "bbbbb" +输出: 1 +解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 +示例 3: + +输入: s = "pwwkew" +输出: 3 +解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 + 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 + + +提示: + +0 <= s.length <= 5 * 104 +s 由英文字母、数字、符号和空格组成 + + + +方式一: + +暴力破解,通过多个循环以穷举的方式 找出答案。简单且易想到的方案,包括之前我们刷的两道算法题也可以使用暴力破解来解决,缺点就是虽然简单,但是效率非常低,并非是最优方案。 + + +具体思路: + +- 将字符串中每个字符作为一个循环,和子串的组合进行比较,从而获取最长字串长度。 + +如字符串abc,则第一轮比较为: 字符a和子串a、ab、abc比较,第二轮则为字符b和子串b、bc比较,以此类推,最后获取不重复的子串长度。 + +```java +public class Solution { + public static int lengthOfLongestSubstring(String s) { + int maxLength = 0; + + for (int i = 0; i < s.length(); i++) { + for (int j = i + 1; j < s.length(); j++) { + if (!judgeCharacterExist(i, j, s)) { + maxLength = Math.max(maxLength, j - i); + } + } + } + return maxLength; + } + + + public static boolean judgeCharacterExist(int start, int end, String param) { + HashSet hashSet = new HashSet<>(); + for (int i = start; i < end; i++) { + if (hashSet.contains(param.charAt(i))) { + return true; + } + hashSet.add(param.charAt(i)); + } + + return false; + } +} +``` + +该方法效率低,时间福再度为O(n³)。 + + +方法二: 滑动窗口法 + +滑动窗口: 是数组和字符串中一个抽象的概念,可分为滑动和窗口两个概念理解。 + +窗口:即表示一个范围,通常是字符串和数组从开始到结束两个索引范围中间包含的一系列元素集合。如字符串abcd,如果开始索引和结束索引分别为0、2的话,这个窗口包含的字符则为: abc。 + +滑动:它表示窗口的开始和结束索引是可以往某个方向移动的。 +如上面的例子开始索引和结束索引分别为0、2的话,当开始索引和结束索引都往右移动一位时,它们的索引值分别为1、3,这个窗口包含的字符为:bcd。 + +示例: + +- abcdef +- 字符串开始索引: 0 +- 字符串结束索引: 5 +- 开始和结束索引范围包含的字符(abcdef)就可以看作是一个窗口 + +思路: + +- 使用一个HashSet来实现滑动窗口,用来检查重复字符 +- 维护开始和结束两个索引,默认都是从0开始 +- 随着循环 向右移动结束索引 + + - 遇到不是重复的字符则放入窗口里 + - 遇到重复字符则向右移动开始索引 + +最终得到结果。 + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + int maxLength = 0; + int leftPoint = 0; + int rightPoint = 0; + + Set set = new HashSet<>(); + + while(leftPoint < s.length() && rightPoint < s.length()) { + if (!set.contains(s.charAt(rightPoint))) { + set.add(s.charAt(rightPoint++)); + maxLength = Math.max(maxLength, rightPoint - leftPoint); + } else { + set.remove(s.charAt(leftPoint++)); + } + } + return maxLength; + } +} +``` + +滑动窗口算法的时间复杂度为O(n),相比于暴力破解,它的效率大大提高了。 + +方式三: + +虽然方式使用了滑动窗口时间复杂度只有O(n),但是如果存在重复字符还需要移动开始索引,因此我们可以考虑借助之前其他算法谈到的“空间换时间”的想法,通过借助HashMap建立字符和索引映射,避免手动移动索引。 + + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + char[] charArry = s.toCharArray(); + + HashMap keyMap = new HashMap<>(); + int maxLength = 0; + + int leftPoint = 0; + for (int i = 0; i < charArry.length; i++) { + if (keyMap.containsKey(charArry[i])) { + // 存在重复数据则获取索引值最大的 + leftPoint = Math.max(leftPoint, keyMap.get(charArry[i])); + } + // 值重复就覆盖,不重复就添加 + keyMap.put(charArry[i], i + 1); + maxLength = Math.max(maxLength, i + 1 - leftPoint); + } + return maxLength; + } +} +``` + +时间复杂度为O(n),虽然和上一种方案的时间复杂度是一样的,但是效率还是有一定的提高(思考问题时要思考是否还有其他有优质的方案,培养发散思维)。 + + +方式四: + + +上面题目分析的时候就提到了要注意题目中提到的:【字符串英文字母、数字、符号和空格组成】,这些字符是可以使用ASCII表示(如字符a的ASCII值为97,想具体了解的可以百度下),那么我们就可以建立字符与ASCII的映射关系,从而实现重复字符的排除。 + + + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + int[] index = new int[128]; + int maxLength = 0; + int length = s.length(); + int i = 0; + for (int j = 0; j < length; j++) { + i = Math.max(index[s.charAt(j)], i); + index[s.charAt(j)] = j + 1; + maxLength = Math.max(maxLength, j + 1 - i); + } + return maxLength; + } +} +``` + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" new file mode 100644 index 00000000..30e2a723 --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" @@ -0,0 +1,585 @@ +八种排序算法 +=== + + +直接插入排序(Straight Insertion Sorting) +--- + +基本思想:在要排序的一组数中,假设前面`(n-1)[n>=2]`个数已经是排好顺序的,现在要把第`n`个数插到前面的有序数中,使得这`n`个数也是排好顺序的。如此反复循环,直到全部排好顺序。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/straight_insertion_sorting.png?raw=true) + +总体分析: + +- 用第二个数和第一个数比较大小,大的放到右边。 +- 用第三个数分别和第二个数还有第一个数比较,并把大的放到右边。 +- 用第四个数分别和第一个第三个第二个第一个数比较,并把大的放到右边。 +- ... + + +所以这里肯定要用到嵌套`for`循环。 + +```java + public void insertSort(int[] arr) { + int len = arr.length; + //要插入的数 + int insertNum; + //因为第一次不用,所以从1开始 + for (int i = 1; i < len; i++) { + insertNum = arr[i]; + //序列元素个数 + int j = i - 1; + //从后往前循环,将大于insertNum的数向后移动 + while (j > 0 && arr[j] > insertNum) { + //元素向后移动 + arr[j + 1] = arr[j]; + j--; + } + //找到位置,插入当前元素 + arr[j + 1] = insertNum; + } +} +``` + +希尔排序 +--- + +针对直接插入排序的下效率问题,有人对次进行了改进与升级,这就是现在的希尔排序。希尔排序,也称递减增量排序算法, +是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。 +希尔排序是`1959`年由`D.L.Shell`提出来的,相对直接插入排序有较大的改进。希尔排序的实质就是分组插入排序,该方法又称缩小增量排序。 + +基本思想:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序, +然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。 +因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的. +因此希尔排序在时间效率上比前两种方法有较大提高。步长的选择是希尔排序的重要部分。 +只要最终步长为1任何步长序列都可以工作。 + + +希尔排序是基于插入排序的以下两点性质而提出改进方法的: + +- 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率 +- 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位 + +对于直接插入排序问题,数据量巨大时。 +将数的个数设为n,取奇数k=n/2,将下标差值为k的数分为一组,构成有序序列。 +再取k=k/2 ,将下标差值为k的书分为一组,构成有序序列。 +重复第二步,直到k=1执行简单插入排序。 + + +算法最开始以一定的步长进行排序,然后会继续以一定步长进行排序,最终算法以步长为1进行排序。 +当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。 +`Donald Shell`最初建议步长选择为\frac{n}{2}并且对步长取半直到步长达到 1。 +虽然这样取可以比\mathcal{O}(n^2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。 + +``` +希尔排序示例:n=10的一个数组 58 27 32 93 65 87 58 46 9 65,步长为n/2。 + +第一次排序 步长为 10/2 = 5 + + 58 27 32 93 65 87 58 46 9 65 + 1A 1B + 2A 2B + 3A 3B + 4A 4B + 5A 5B + + +首先将待排序元素序列分组,以5为步长,(1A,1B),(2A,2B),(3A,3B)等为分组标记,大写字母表示是该组的第几个元素, +数字相同的表示在同一组,这样就分成5组,即(58,87),(27,58),(32,46),(93,9),(65,65), +然后分别对各分组进行直接插入排序,排序后5组为(58,87),(27,58),(32,46),(9,93),(65,65), +分组排序只是变得各个分组内的下表,下同。 + +第二次排序 步长为 5/2 = 2 + +58 27 32 9 65 87 58 46 93 65 + +1A 1B 1C 1D 1E + + 2A 2B 2C 2D 2E + +第三次排序 步长为 2/2 = 1 + +32 9 58 27 58 46 65 65 93 87 + +1A 1B 1C 1D 1E 1F 1G 1H 1I 1J + +第四次排序 步长为 1/2 = 0 得到有序元素序列 + +9 27 32 46 58 58 65 65 87 93 + +``` + + +```java + public void shellSort(int[] arr) { + int len = arr.length; + while (len != 0) { + len = len / 2; + //分组 + for (int i = 0; i < len; i++) { + //元素从第二个开始 + for (int j = i + len; j < arr.length; j += len) { + //k为有序序列最后一位的位数 + int k = j - len; + //要插入的元素 + int temp = arr[j]; + //从后往前遍历 + while (k >= 0 && temp < arr[k]) { + arr[k + len] = arr[k]; + //向后移动len位 + k -= len; + } + arr[k + len] = temp; + } + } + } +} +``` + + +简单选择排序 +--- + +基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换; +然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。 + +思路: + +- 第一次用第一个数字去和后面的每个数字比较,将小的放到第一个位置,这样完成这一轮之后,第一个数就是最小的数。 +- 第二次用第二个数字去和后面的每个数字比较,将晓得放到第二个位置,这样完成这一轮之后,第二个数就是最第二小的数。 +- ... + + +```java +public void selectSort(int[] arr) { + int len = arr.length; + for (int i = 0; i < len; i++) { + int value = arr[i]; + int position = i; + //找到最小的值和位置 + for (int j = i + 1; j < len; j++) { + if (arr[j] < value) { + value = arr[j]; + position = j; + } + } + //进行交换 + arr[position] = arr[i]; + arr[i] = value; + } + } +``` + + +堆排序 +--- + +对简单选择排序的优化。 +基本思想:堆排序是一种树形选择排序,是对直接选择排序的有效改进。 + +堆`(heap)`,这里所说的堆是数据结构中的堆,而不是内存模型中的堆。 + +堆数据结构是一种特殊的二叉树,在这棵树中,所有父节点都满足大于等于其子节点的堆叫大根堆,所有父节点都满足小于等于其子节点的堆叫小根堆。堆虽然是一颗树,但是通常存放在一个数组中,父节点和孩子节点的父子关系通过数组下标来确定。如下图的小根堆及存储它的数组: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/heap_1.png?raw=true) + +堆的介绍: + +- 堆是完全二叉树 +- 常常用数组实现 +- 每一个节点的关键字都大于(等于)这个节点的子节点的关键字 + +由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。 +初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。 +然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。 +从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。 + + +步骤: + +- 将序列构建成大顶堆。 +- 将根节点与最后一个节点交换,然后断开最后一个节点。 +- 重复第一、二步,直到所有节点断开。 + +```java +public void heapSort(int[] arr) { + int len = arr.length; + //循环建堆 + for (int i = 0; i < len - 1; i++) { + //建堆 + buildMaxHeap(arr, len - 1 - i); + //交换堆顶和最后一个元素 + swap(arr, 0, len - 1 - i); + } + } + + //交换方法 + private void swap(int[] data, int i, int j) { + int tmp = data[i]; + data[i] = data[j]; + data[j] = tmp; + } + + //对data数组从0到lastIndex建大顶堆 + private void buildMaxHeap(int[] data, int lastIndex) { + //从lastIndex处节点(最后一个节点)的父节点开始 + for (int i = (lastIndex - 1) / 2; i >= 0; i--) { + //k保存正在判断的节点 + int k = i; + //如果当前k节点的子节点存在 + while (k * 2 + 1 <= lastIndex) { + //k节点的左子节点的索引 + int biggerIndex = 2 * k + 1; + //如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在 + if (biggerIndex < lastIndex) { + //若果右子节点的值较大 + if (data[biggerIndex] < data[biggerIndex + 1]) { + //biggerIndex总是记录较大子节点的索引 + biggerIndex++; + } + } + //如果k节点的值小于其较大的子节点的值 + if (data[k] < data[biggerIndex]) { + //交换他们 + swap(data, k, biggerIndex); + //将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值 + k = biggerIndex; + } else { + break; + } + } + } + } + +``` + + +冒泡排序 +--- + +步骤: + +- 将序列中所有元素两两比较,将最大的放在最后面。 +- 将剩余序列中所有元素两两比较,将最大的放在最后面。 +- 重复第二步,直到只剩下一个数。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/bublle_sort.png?raw=true) + + +```java +public void bubbleSort(int[] arr) { + int len = arr.length; + for (int i = 0; i < len; i++) { + for (int j = 0; j < len - i - 1; j++) { + if (arr[j] > arr[j + 1]) { + int temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } + } +``` + + +快速排序 +--- + +基本思想:找出一个元素(理论上可以随便找一个)作为基准`(pivot)`,然后对数组进行分区操作, +使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置。 +递归快速排序,将其他`n-1`个元素也调整到排序后的正确位置。最后每个元素都是在排序后的正 确位置,排序完成。 +所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归。 + +### 选择基准元 + +1. 固定基准元 + 如果输入序列是随机的,处理时间是可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为冒泡排序,时间复杂度为Θ(n^2)。而且,输入的数据是有序或部分有序的情况是相当常见的。因此,使用第一个元素作为基准元是非常糟糕的,应该立即放弃这种想法。 +2. 随机基准元 + 这是一种相对安全的策略。由于基准元的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(n×log(n))的期望时间复杂度。 +3. 三数取中 + 最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为基准元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。 + + +算法原理:单单看以上解释还是有些模糊,可以通过实例来理解它。 +下面通过一组数据来进行排序过程的解: + +原数组:{3,7,2,9,1,4,6,8,10,5} + +花了点时间撸了下面这张快速排序示意图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/quick_sort.png?raw=true) + + +### partition算法 + +`partition`算法是快速排序的核心,在学习快排之前,可以先学习一下这个算法。下面先贴代码: + +```java +public int partition(int[] num, int left, int right) { + if (num == null || num.length <= 0 || left < 0 || right >= num.length) { + return 0; + } + //获取数组中间元素的下标 + int prio = num[left + (right - left) / 2]; + //从两端交替向中间扫描 + while (left <= right) { + while (num[left] < prio) + left++; + while (num[right] > prio) + right--; + if (left <= right) { + //最终将基准数归位 + swap(num, left, right); + left++; + right--; + } + } + return left; +} +``` + +这个方法的思路是先找一个枢纽元(这个方法实现里面找的是第一个元素,具体其实大有文章不过这里先简化描述), +再从数组的两边(具体从哪里到哪里由传进来额参数决定)生成两个指针`left`和`right`, +每次发现左边的元素大于枢纽元则i停下来,右边的元素小于枢纽元j就停下来,并且交换这个两个数的位置。 +直到两个指针`left`,`right`相遇。再把枢纽元插入`left`的位置,也就是它应该在的位置。 + +这么做最后的结果是让数组的`[left,right]`部分呈现出2部分,枢纽元最终位置以左都是小于等于枢纽元的,以右都是大于等于枢纽元的。而枢纽元则被插入到了一个绝对正确的位置。 + +```java +@Override +protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + int[] data = new int[]{26, 53, 67, 48, 57, 13, 48, 32, 60, 50}; + quickSort(data,0,data.length-1); +} + +void quickSort(int arr[], int left, int right) { + if (left < right) { + //算出枢轴值 + int index = partition(arr, left, right); + //对低子表递归排序 + quickSort(arr, left, index - 1); + //对高子表递归排序 + quickSort(arr, index + 1, right); + } + + for (int i = 0; i < arr.length; i++) { + Log.e("@@@", "" + arr[i]); + } +} + +public int partition(int[] num, int left, int right) { + if (num == null || num.length <= 0 || left < 0 || right >= num.length) { + return 0; + } + int prio = num[left + (right - left) / 2]; //获取数组中间元素的下标 + while (left <= right) { //从两端交替向中间扫描 + while (num[left] < prio) + left++; + while (num[right] > prio) + right--; + if (left <= right) { + swap(num, left, right); //最终将基准数归位 + left++; + right--; + } + } + return left; +} + + +public void swap(int[] num, int left, int right) { + int temp = num[left]; + num[left] = num[right]; + num[right] = temp; +} +``` + +归并排序 +--- + +速度仅次于快速排序,内存少的时候使用,可以进行并行计算的时候使用。 +归并(`Merge`)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列, +每个子序列是有序的。然后再把有序子序列合并为整体有序序列。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/merge_sort.png?raw=true) + + +```java +{ + int[] data = new int[]{26, 53, 67, 48, 57, 13, 48, 32, 60, 50}; + data = mergeSort(data, 0, data.length - 1); + for (int i = 0; i < data.length; i++) { + Log.e("@@@", "" + data[i]); + } +} + +public static int[] mergeSort(int[] a, int low, int high) { + int mid = (low + high) / 2; + if (low < high) { + mergeSort(a, low, mid); + mergeSort(a, mid + 1, high); + //左右归并 + merge(a, low, mid, high); + } + return a; +} + +public static void merge(int[] a, int low, int mid, int high) { + int[] temp = new int[high - low + 1]; + int i = low; + int j = mid + 1; + int k = 0; + // 把较小的数先移到新数组中 + while (i <= mid && j <= high) { + if (a[i] < a[j]) { + temp[k++] = a[i++]; + } else { + temp[k++] = a[j++]; + } + } + // 把左边剩余的数移入数组 + while (i <= mid) { + temp[k++] = a[i++]; + } + // 把右边边剩余的数移入数组 + while (j <= high) { + temp[k++] = a[j++]; + } + // 把新数组中的数覆盖nums数组 + for (int x = 0; x < temp.length; x++) { + a[x + low] = temp[x]; + } +} +``` + +基数排序 +--- + +基数排序`(radix sort)`又称桶排序`(bucket sort)`,相对于常见的比较排序,基数排序是一种分配式排序, +即通过将所有数字分配到应在的位置最后再覆盖到原数组完成排序的过程 + + +用于大量数,很长的数进行排序时。 + +基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始, +依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。 + +我们回想一下我们小时候是怎么学习比较数字大小的?我们是先比位数,如果一个位数比另一个位数多, +那这个数肯定更大。如果位数同样多,就按位数递减依次往下进行比较,哪个数在这一位上更大那就停止比较, +得出这个在这个位上数更大的数字整体更大的结论。当然我们也可以从最小的位开始比较, +这其实就对应了基数排序里的`MSD(most significant digital)和LSD(least significant digital)`两种排序方式。 + +想清楚了这一点之后,我们就要考虑如何存储每一位排序结果的问题了,首先既然作为分配式排序,联想计数排序, +每一位排序时存储该次排序结果的数据结构应该至少是一个长度为10的数组(对应十进制该位0-9的数字)。 +同时可能存在以下情况:原数组中所有元素在该位上的数字都相同,那一维数组就没法满足我们的需要了, +我们需要一个`10*n`(`n`为数组长度)的二维数组来存储每次位排序结果。 + +熟悉计数排序结果的读者可能会好奇:为什么不能像计数排序一样,在每个位置只存储出现该数字的次数, +而不存储具体的值,这样不就可以用一维数组了?这个我们不妨先思考一下,在对基数排序分析完之后再来看这个问题。 +现在我们可以存储每次位排序的结果了,为了在下一位排序前用到这一位排序的结果, +我们要将桶里排序的结果还原到原数组中去,然后继续对更改后的原数组执行前一步的位排序操作,如此循环, +最后的结果就是数组内元素先按最高位排序,最高位相同的依次按下一位排序,依次递推。得到排序的结果数组。 + + +初始化:构造一个`10*n`的二维数组,一个长度为n的数组用于存储每次位排序时每个桶子里有多少个元素。 +循环操作:从低位开始(我们采用`LSD`的方式),将所有元素对应该位的数字存到相应的桶子里去(对应二维数组的那一列)。 +然后将所有桶子里的元素按照桶子标号从小到大取出,对于同一个桶子里的元素,先放进去的先取出, +后放进去的后取出(保证排序稳定性)。这样原数组就按该位排序完毕了,继续下一位操作,直到最高位排序完成。 + +下面给出一个实例帮助理解: +我们现有一个数组:73, 22, 93, 43, 55, 14, 28, 65, 39, 81 +下面是排序过程(二维数组里每一列对应一个桶,因为桶空间没用完,因此没有将二维数组画全): + +- 按个位排序 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/radix_sort_1.png?raw=true) + +按第一位排序后数组结果: +81,22,73,93,43,14,55,65,28,39 +可以看到数组已经按个位排序了。 + +- 根据个位排序结果按百位排序 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/radix_sort_2.png?raw=true) + + +取出排序结果: +14,22,28,39,43,55,65,73,81,93 + +可以看到在个位排序的基础上,百位也排序完成(对于百位相同的数子,如22,28,因为个位已经排序, +而取出时也保持了排序的稳定性,所以这两个数的位置前后是根据他们个位排序结果决定的)。 +因为原数组元素最高只有百位,原数组也完成了排序过程。 + +我们现在来看看之前遗留的两个问题:为什么不能用一维数组, +一定要用二维数组这样的类似桶的结构来存储中间位排序结果?其实之所以要写这个问题, +是因为我觉得这个问题是理解基数排序的关键。基数排序本身原理很简单, +但是实现中有两个问题需要考虑:1.怎么保留前一位的排序结果,这个问题用之前提到的排序稳定性可以解决。 +2.怎么关联该位排序结果和原数组元素,二维数组正是为了解决这个问题使用的办法。在计数排序里, +虽然保留了所有相等的元素的相对位置,但是这些相等的元素在计数排序里实际是没有差别的, +因此我们可以只保存数组里有多少个这样的元素即可。而基数排序里不同,有些元素虽然在某一位上相同, +但是他们其他位上很可能不同,如果只保存该位上有多少个5或者多少个6,那关于元素其他位的信息就都丢弃了, +这样也就没法对这些元素更高位进行排序了。 + +```java +private static void radixSort(int[] array,int d) { + int n=1;//代表位数对应的数:1,10,100... + int k=0;//保存每一位排序后的结果用于下一位的排序输入 + int length=array.length; + int[][] bucket=new int[10][length];//排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里 + int[] order=new int[length];//用于保存每个桶里有多少个数字 + while(n The Stack class represents a last-in-first-out (LIFO) stack of objects. It extends class Vector with five operations +> that allow a vector to be treated as a stack. The usual push and pop operations are provided, as well as a method to +> peek at the top item on the stack, a method to test for whether the stack is empty, and a method to search the stack +> for an item and discover how far it is from the top. +> When a stack is first created, it contains no items. + +栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫作栈顶,数据称为压栈,移除数据称为弹栈(就像子弹弹夹装弹和取弹一样)。 +对栈的基本操作有`push`(进栈)和`pop`(出栈),前者相当于插入,后者相当于删除最后一个元素。栈有时又叫作`LIFO(Last In First Out)`表, +即后进先出。简单暴力的理解就是吃进去吐出来 + +优点: + +- 提供了先进后出的存取方式 + +缺点: + +- 存取其他项很慢 + + +### 链栈 + +栈的顺序存储结构,我们现在来看看栈的链式存储结构,简称为链栈。 +对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真的发生,那此时的计算机操作系统已经面临死机崩溃的情况,而不是这个链栈是 +否溢出的问题。 +对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题, +但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的 +一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。 + + +#### 栈的应用-递归 + +把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。 + +当然,写递归程序最怕的就是陷入永不结束的无穷递归中,所以,每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。 + +递归(recursion)是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段: + +- 递:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。 +- 归:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。 + +而从实现的角度看,递归代码主要包含三个要素: + +- 终止条件:用于决定什么时候由“递”转“归”。 +- 递归调用:对应“递”,函数调用自身,通常输入更小或更简化的参数。 +- 返回结果:对应“归”,将当前递归层级的结果返回至上一层。 + + +那么我们讲了这么多递归的内容,和栈有什么关系呢?这得从计算机系统的内部说起。前面我们已经看到递归是如何执行它的前行和退回阶段的。 +递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。这种存储某些数据, +并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然很符合栈这样的数据结构,因此,编译器使用栈实现递归就没什么好惊讶的了。 +简单的说,就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出, +用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。 + + +### 队列`(Queue)` + +队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。 +队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。 + +队列是一种特殊的线性表,特殊之处在于它只允许在表的前端`(front)`进行删除操作,而在表的后端`(rear)`进行插入操作,和栈一样,队列是一种操作受限制 +的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。先进先出,简单暴力的理解就是吃进去拉出来 + + +优点: + +- 提供了先进先出的存取方式 + +缺点: + +- 存取其他项很慢 + +### 链队列 +队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。 + + +### 串 +串(string)是由零个或多个字符组成的有限序列,又名叫字符串。 + + + +### 树`(Tree)` + +树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中: +- 有且仅有一个特定的称为根(Root)的结点; +- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。 + + +树是由`n(n>=1)`个有限节点组成一个具有层次关系的集合。 +它具有以下特点:每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树。 + +树中结点的最大层次称为树的深度(Depth)。如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。 + + +#### 二叉树 + +二叉树`(binary tree)`是一棵树,每一个节点都不能有多于两个的子节点。 +通常子树被称作“左子树”和“右子树”。二叉树常被用于实现二叉查找树和二叉堆。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binary_tree.jpg?raw=true) + + +二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_ercha_list.png?raw=true) +其中data是数据域,lchild和rchild都是指针域,分别存放只想左孩子和右孩子的指针。 +如果有需要,还可以再增加一个指向其双亲的指针域,那样就称之为三叉链表 + +二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。 +##### 二叉树遍历方法 +- 前序遍历 + 规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。 +- 中序遍历 + 规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。 +- 后序遍历 + 规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。 +- 层序遍历 + 规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。 + + + +#### 满二叉树和完全二叉树 + +满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。 + +完全二叉树:若设二叉树的深度为`h`,除第`h`层外,其它各层`(1~(h-1))`层的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/wanquan_binary_tree.jpg?raw=true) + + +完全二叉树是效率很高的数据结构,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法、Dijkstra算法、Prim算法等都要用堆才 +能优化,二叉排序树的效率也要借助平衡性来提高,而平衡性基于完全二叉树。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/man_binary_tree.png?raw=true) + + + +我们知道一颗基本的二叉排序树他们都需要满足一个基本性质:即树中的任何节点的值大于它的左子节点,且小于它的右子节点。 + +按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉排序树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树), +这样势必会导致二叉树的检索效率大大降低`(O(n))`,所以为了维持二叉排序树的平衡,大牛们提出了各种平衡二叉树的实现算法,在平衡二叉搜索树中, +其高度一般都良好地维持在`O(log2n)`,大大降低了操作的时间复杂度。如:`AVL`,`SBT`,伸展树,`TREAP` ,红黑树等等。 + + +#### 平衡二叉树 + +平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一 +个子节点,其左右子树的高度都相近。下面给出平衡二叉树的示意图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/pingheng_binary_tree.jpg?raw=true) + + +#### 红黑树 + +红黑树顾名思义就是结点是红色或者是黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。红黑树是一种自平衡二叉查找树,是在计算机科学中用到的 +一种数据结构,典型的用途是实现关联数组。它是在1972年由`Rudolf Bayer`发明的,他称之为"对称二叉B树",它现代的名字是在`Leo J. Guibas`和 +`Robert Sedgewick`于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在`O(log n)` +时间内做查找,插入和删除,这里的`n`是树中元素的数目。 + +对于一棵有效的红黑树而言我们必须增加如下规则,这也是红黑树最重要的5点规则: + +- 每个结点都只能是红色或者黑色中的一种。 +- 根结点是黑色的。 +- 每个叶结点(NIL节点,空节点)是黑色的。 +- 如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。 +- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结 + +这些约束强制了红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hongheishu.jpg?raw=true) + +黑红树节点的`java`表示结构: + +```java +private static final boolean RED = true; +private static final boolean BLACK = false; +private Node root;//二叉查找树的根节点 + +//结点数据结构 +private class Node{ + private Key key;//键 + private Value value;//值 + private Node left, right;//指向子树的链接:左子树和右子树. + private int N;//以该节点为根的子树中的结点总数 + boolean color;//由其父结点指向它的链接的颜色也就是结点颜色. + + public Node(Key key, Value value, int N, boolean color) { + this.key = key; + this.value = value; + this.N = N; + this.color = color; + } +} + +/** + * 获取整个二叉查找树的大小 + * @return + */ +public int size(){ + return size(root); +} +/** + * 获取某一个结点为根结点的二叉查找树的大小 + * @param x + * @return + */ +private int size(Node x){ + if(x == null){ + return 0; + } else { + return x.N; + } +} +private boolean isRed(Node x){ + if(x == null){ + return false; + } + return x.color == RED; +} +``` + +哈希表`(Hash)` +--- + +哈希表就是一种以 键-值`(key-indexed)`存储数据的结构,我们只要输入待查找的值即`key`,即可查找到其对应的值。 + +优点: + +- 如果关键字已知则存取极快 +- 插入、查找、删除的时间级为`O(1)` +- 数据项占哈希表长的一半,或者三分之二时,哈希表的性能最好。 + +缺点: + +- 删除慢,如果不知道关键字存取慢,对存储空间使用不充分 +- 基于数组,数组创建后难于扩展,某些哈希表被基本填满时性能下降的非常严重; +- 没有一种简单的方法可以以任何一种顺序(如从小到大)遍历整个数据项; + +堆`(Heap)` +--- + +这里所说的堆是数据结构中的堆,而不是内存模型中的堆。堆通常是一个可以被看做一棵树,它满足下列性质: + +- 堆中任意节点的值总是不大于(不小于)其子节点的值; +- 堆是完全二叉树 +- 常常用数组实现 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/heap_1.png?raw=true) + +二叉堆是完全二元树或者是近似完全二元树,它分为两种:最大堆和最小堆。 +最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。 + +优点: + +- 插入、删除快,对最大数据项存取快 + +缺点: + +- 对其他数据项存取慢 + + +图`(Graph)` +--- + +图是一种较线性表和树更为复杂的数据结构,在线性表中,数据元素之间仅有线性关系,在树形结构中,数据元素之间有着明显的层次关系,而在图形结构中, +节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。图的应用相当广泛,特别是近年来的迅速发展,已经渗入到诸如语言学、逻辑学、物理、 +化学、电讯工程、计算机科学以及数学的其他分支中。 +图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。 + + + +优点: + +- 对现实世界建模 + +缺点: + +- 有些算法慢且复杂 + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" new file mode 100644 index 00000000..b33352bd --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" @@ -0,0 +1,333 @@ +算法 +=== + + +算法(algorithm)是在有限时间内解决特定问题的一组指令或操作步骤,它具有以下特性: + +- 问题是明确的,包含清晰的输入和输出定义。 +- 具有可行性,能够在有限步骤、时间和内存空间下完成。 +- 各步骤都有确定的含义,在相同的输入和运行条件下,输出始终相同。 + + +算法具有五个基本特性: +- 输入 + 算法具有零个或多个输入 +- 输出 + 算法至少有一个或多个输出 +- 有穷性 + 指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。 +- 确定性 + 算法的每一步骤都具有确定的含义,不会出现二义性 +- 可行性 + 算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成 + + +在算法设计中,我们先后追求以下两个层面的目标。 + +- 找到问题解法:算法需要在规定的输入范围内可靠地求得问题的正确解。 +- 寻求最优解法:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。 + +也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度: + +- 时间效率:算法运行时间的长短。 +- 空间效率:算法占用内存空间的大小。 + +简而言之,我们的目标是设计“既快又省”的数据结构与算法。而有效地评估算法效率至关重要,因为只有这样,我们才能将各种算法进行对比,进而指导算法设计与优化过程。 + + + + + +### 算法时间复杂度 + +一个算法的时间复杂度(Time Complexity)是指算法运行从开始到结束所需要的时间。这个时间就是该算法中每条语句的执行时间之和,而每条语句的执行时间是该语句执行次数(也称为频度)与执行该语句所需时间的乘积。但是,当算法转换为程序之后,一条语句执行一次所需的时间与机器的性能及编译程序生成目标代码的质量有关,是很难确定的。为此,假设执行每条语句所需的时间均为单位时间。在这一假设下,一个算法所花费的时间就等于算法中所有语句的频度之和。这样就可以脱离机器的硬、软件环境而独立地分析算法所消耗的时间。 + +在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。 + +这样用大写O( )来体现算法时间复杂度的记法,我们称之为大O记法。 +一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。 +显然,由此算法时间复杂度的定义可知,我们的三个求和算法的时间复杂度分别为O(n),O(1),O(n²)。 +我们分别给它们取了非官方的名称,O(1)叫常数阶、O(n)叫线性阶、O(n²)叫平方阶。 + +- O(1) : 操作所需的时间是常数(n个元素所需的时间与一个元素所需的时间相同) +- O(n) : n个元素的时间是一个元素时间乘以n +- O(n²): n个元素的时间是一个元素时间乘以n² + +#### 推导大O阶方法 + +推导大O阶: +1. 用常数1取代运行时间中的所有加法常数。 +2. 在修改后的运行次数函数中,只保留最高阶项。 +3. 如果最高阶项存在且不是1,则去除与这个项相乘的常数。 +得到的结果就是大O阶。 + +##### 常数阶 + +顺序结构的时间复杂度。 + +下面这个算法,也就是刚才的第二种算法(高斯算法),为什么时间复杂度不是O(3),而是O(1)。 +``` +int sum = 0, n = 100; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +printf("%d", sum); // 执行一次 +``` +这个算法的运行次数函数是f(n)=3。 +根据我们推导大O阶的方法,第一步就是把常数项3改为1。 +在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1)。 +那假设是这样: + +``` +int sum = 0, n = 100; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +printf("%d", sum); // 执行一次 +``` +事实上无论n为多少,上面的两段代码就是3次和12次执行的差异。这种与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶。注意:不管这个常数是多少,我们都记作O(1),而不能是O(3)、O(12)等其他任何数字,这是初学者常常犯的错误。 + + +##### 线性阶 + +循环结构的时间复杂度。 + +下面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码须要执行n次。 +```java +int i; +for (i = 0; i < n; i++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... +} + +``` +##### 对数阶 +下面的这段代码,时间复杂度是多少呢? +```java +int count = 1; +while (count < n) { + count = count * 2; + // 下面省略时间复杂度为O(1)的程序步骤 + ... +} +``` +由于每次count乘以2之后,就距离n更近了一分。 +也就是说,有多少个2相乘后大于n,则会退出循环。 +由2^x = n得到 x = ㏒(2)N。所以这个循环的时间复杂度为O(㏒n) + + +##### 平方阶 + +下面的例子是一个循环嵌套,它的内循环上面已经分析过,时间复杂度为O(n)。 +```java +int i, j; +for (i = 0; i < n; i ++ ) { + for (j = 0; j < n; j ++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` + +而对于外层的循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以这段代码的时间复杂度为O(n2)。 + +如果外循环的循环次数改为了m,时间复杂度就变为O(m×n),如下: +```java +int i, j; +for (i = 0; i < m; i ++ ) { + for (j = 0; j < n; j ++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` +所以我们可以总结得出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。那么下面这个循环嵌套,它的时间复杂度是多少呢? +```java +int i, j; +for(i = 0; i < n; i ++) { + for (j = i; j < n; j ++) { // 注意j = i 而不是0 + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` +由于当i=0时,内循环执行了n次,当i=1时,执行了n-1次,……当i=n-1时,执行了1次。所以总的执行次数为: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_1.png?raw=true) +用我们推导大O阶的方法: +- 第一条,没有加法常数不予考虑 +- 第二条,只保留最高阶项,因此保留n2/2 +- 第三条,去除这个项相乘的常数,也就是去除1/2 +最终这段代码的时间复杂度为O(n2)。 + +##### 常见的时间复杂度 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_2.png?raw=true) + + + +### 算法空间复杂度 + +我们在写代码时,完全可以用空间来换取时间,比如说,要判断某某年是不是闰年,你可能会花一点心思写了一个算法,而且由于是一个算法,也就意味着,每次给一个年份,都是要通过计算得到是否是闰年的结果。还有另一个办法就是,事先建立一个有2050个元素的数组(年数略比现实多一点),然后把所有的年份按下标的数字对应,如果是闰年,此数组项的值就是1,如果不是值为0。这样,所谓的判断某一年是否是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,我们的运算是最小化了,但是硬盘上或者内存中需要存储这2050个0和1。这是通过一笔空间上的开销来换取计算时间的小技巧。到底哪一个好,其实要看你用在什么地方。 + +算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。 + +一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)。 + + +算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度`S(n)`定义为该算法所耗费空间的数量级。 +`S(n)=O(f(n))`若算法执行时所需要的辅助空间相对于输入数据量`n`而言是一个常数,则称这个算法的辅助空间为`O(1)`; +递归算法的空间复杂度:递归深度`N*`每次递归所要的辅助空间,如果每次递归所需的辅助空间是常数,则递归的空间复杂度是`O(N)`. + +空间复杂度的分析方法: + +- 一个算法的空间复杂度`S(n)`定义为该算法所耗费的存储空间,它也是问题规模`n`的函数。空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。 +- 一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。 +- 一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。 + +  算法的空间复杂度一般也以数量级的形式给出。如当一个算法的空间复杂度为一个常量,即不随被处理数据量`n`的大小而改变时,可表示为`O(1)`; +  当一个算法的空间复杂度与以2为底的`n`的对数成正比时,可表示为`O(log2n)`; +  当一个算法的空间复杂度与`n`成线性比例关系时,可表示为`O(n)`。若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。 + +空间复杂度补充: + +一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。 +一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。 +程序执行时所需存储空间包括以下两部分:    + +- 固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。 +- 可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。一个算法所需的存储空间用`f(n)`表示。`S(n)=O(f(n))`其中`n`为问题的规模,`S(n)`表示空间复杂度。 + + +时间与空间复杂度比较 +--- + +对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差, +即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。 +另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。 + + +通常,我们都使用“时间复杂度”来指运行时间的需求,使用“空间复杂度”指空间需求。 + +当不用限定词地使用“复杂度”时,通常都是指时间复杂度。 + + + + +## 顺序查找算法 + +顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。 + +算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题, +执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。 +算法可以理解为有基本运算及规定的运算顺序所构成的完整的解题步骤。或者看成按照要求设计好的有限的确切的计算序列,并且这样的步骤和序列可以解决一类问题。 +```java +/** + * 1到100所有素数的和 + * 素数就是只能够被1和自身整除的数 + */ +public class dd { + public static void main(String[] args) { + int i = 2; // i 即为所求素数 + System.out.println("i= " + i); + for (i = 3; i <= 100; i = i + 2) { + boolean f = true; + Label: for (int j = 2; j < i; j++) { + if (i % j == 0) { // if(true)时,i为非素数 + f = false; + break Label; // 加了Label貌似只是起到提高效率 + } + } + if (f) {// 当f=true时,i为素数。 + System.out.println("i= " + i); + } + } + } +} + +/** + *古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死, + *问每个月的兔子总数为多少? + * + */ +public class aa{ + public static void main(String[] args) { + int i = 0; + for (i = 1; i < 20; i++) { + System.out.println(f(i)); + } + } + + public static int f(int x) { + if(x==1||x==2) { + return 1; + }else { + return f(x-1)+f(x-2); + } + } +} +``` + +3.有两个有序数组`a`,`b`,现需要将其合并成一个新的有序数组。 + +简单的思路就是先放到一个新的数组中,再排序。但是这样的没体现任何算法,这里考的不是快速排序等排序算法。关键应该是如何利用`有序` 已知这个条件。可以这样想,假设两个源数组的长度不一样,那么假设其中短的数组用完了,即全部放入到新数组中去了,那么长数组中剩下的那一段就可以直接拿来放入到新数组中去了。 + +其中用到的思想是:归并排序思想 + +```java +public static int[] merge(int[] a, int[] b) { + int lengthA = a.length; + int lengthB = b.length; + int[] result = new int[lengthA + lengthB]; + + //aIndex:用于标示a数组 bIndex:用来标示b数组 resultIndex:用来标示传入的数组 + int aIndex = 0, bIndex = 0, resultIndex = 0; + + while (aIndex < lengthA && bIndex < lengthB) { + if (a[aIndex] < b[bIndex]) { + result[resultIndex++] = a[aIndex]; + aIndex++; + } else { + result[resultIndex++] = b[bIndex]; + bIndex++; + } + } + + // a有剩余 + while (aIndex < lengthA) { + result[resultIndex++] = a[aIndex]; + aIndex++; + } + + // b有剩余 + while (bIndex < lengthB) { + result[resultIndex++] = b[bIndex]; + bIndex++; + } + return result; +} +``` + + +数据结构与算法高度相关、紧密结合,具体表现在以下三个方面: + +- 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及操作数据的方法。 +- 算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,结合算法才能解决特定问题。 +- 算法通常可以基于不同的数据结构实现,但执行效率可能相差很大,选择合适的数据结构是关键。 + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/algorithm_datasturcture.jpg?raw=true) + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/\346\255\273\351\224\201.md" "b/JavaKnowledge/\346\255\273\351\224\201.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\346\255\273\351\224\201.md" rename to "JavaKnowledge/\346\255\273\351\224\201.md" diff --git "a/Java\345\237\272\347\241\200/\347\224\237\344\272\247\350\200\205\346\266\210\350\264\271\350\200\205.md" "b/JavaKnowledge/\347\224\237\344\272\247\350\200\205\346\266\210\350\264\271\350\200\205.md" similarity index 100% rename from "Java\345\237\272\347\241\200/\347\224\237\344\272\247\350\200\205\346\266\210\350\264\271\350\200\205.md" rename to "JavaKnowledge/\347\224\237\344\272\247\350\200\205\346\266\210\350\264\271\350\200\205.md" diff --git "a/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" "b/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" new file mode 100644 index 00000000..a638c653 --- /dev/null +++ "b/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" @@ -0,0 +1,261 @@ +线程池的原理 +=== + +所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务。线程池的关键在于它为我们管理了多个线程,我们不需要关心如何创建线程,我们只需要关系我们的核心业务,然后需要线程来执行任务的时候从线程池中获取线程。任务执行完之后线程不会被销毁,而是会被重新放到池子里面,等待机会去执行任务。 + + + +- 在什么情况下使用线程池? + - 单个任务处理的时间比较短 + - 将要处理的任务的数量大 + + 假设一个服务器完成一项任务所需时间为:`T1`创建线程时间,`T2`在线程中执行任务的时间,`T3`销毁线程时间。如果:`T1 + T3`远大于`T2`,则可以采用线程池,以提高服务器性能。 + + +- 一个线程池包括以下四个基本组成部分: + + - 线程池管理器(`ThreadPool`):用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务 + - 工作线程(`PoolWorker`):线程池中的线程,在没有任务时处于等待状态,可以循环的执行任务 + - 任务接口(`Task`):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等 + - 任务队列(`TaskQueue`):用于存放没有处理的任务。提供一种缓冲机制。 + +- 使用线程池的好处: + - 降低资源消耗。通过重复利用减少在创建和销毁线程上所花的时间以及系统资源的开销 + - 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, + 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 + - 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 + +- 工作流程 + 线程池的任务就在于负责这些线程的创建,销毁和任务处理参数传递、唤醒和等待。 + - 创建若干线程,置入线程池 + - 任务达到时,从线程池取空闲线程 + - 取得了空闲线程,立即进行任务处理 + - 否则新建一个线程,并置入线程池,并执行上一步 + - 如果创建失败或者线程池已满,根据设计策略选择返回错误或将任务置入处理队列,等待处理 + - 销毁线程池 + +- 风险 + 虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。 + 用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁, + 它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。 + +- 资源不足 + 线程池的一个优点在于:相对于其它替代调度机制而言,它们通常执行的很好。但只有恰当地调整了线程池大小时才是这样的。 + 线程消耗包括内存和其它系统资源在内的大量资源。除了`Thread`对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。 + 除此以外`JVM`可能会为每个`Java`线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后虽然线程之间切换的调度开销很小, + 但如果有很多线程,环境切换也可能严重地影响程序的性能。 + 如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间, + 而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。 + 除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如`JDBC`连接、套接字或文件。 + 这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配`JDBC`连接。 + + +线程池的使用 +--- + + +`java.uitl.concurrent.ThreadPoolExecutor`类是线程池中最核心的一个类: + +```java +public class ThreadPoolExecutor extends AbstractExecutorService { + ..... + public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, + BlockingQueue workQueue); + + public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, + BlockingQueue workQueue,ThreadFactory threadFactory); + + public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, + BlockingQueue workQueue,RejectedExecutionHandler handler); + + public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, + BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler); + ... +} +``` + +上面构造器中各参数的含义: + +- `corePoolSize`:用来表示线程池中的核心线程的数量,也可以称为可闲置的线程数量。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务, +除非调用了`prestartAllCoreThreads()`或者`prestartCoreThread()`方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建`corePoolSize`个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到`corePoolSize`后,就会把到达的任务放到缓存队列当中; + +- `maximumPoolSize`:来表示线程池中最多能够创建的线程数量。这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程; +- `keepAliveTime`:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于`corePoolSize`时,`keepAliveTime`才会起作用,直到线程池中的线程数不大于`corePoolSize`,即当线程池中的线程数大于`corePoolSize`时,如果一个线程空闲的时间达到`keepAliveTime`,则会终止,直到线程池中的线程数不超过`corePoolSize`。但是如果调用了`allowCoreThreadTimeOut(boolean)`方法,在线程池中的线程数不大于`corePoolSize`时,`keepAliveTime`参数也会起作用,直到线程池中的线程数为0; +- `unit`:参数`keepAliveTime`的时间单位,有7种取值,在`TimeUnit`类中有7种静态属性: + + - `TimeUnit.DAYS` + - `TimeUnit.HOURS` + - `TimeUnit.MINUTES` + - `TimeUnit.SECONDS` + - `TimeUnit.MILLISECONDS` + - `TimeUnit.MICROSECONDS` + - `TimeUnit.NANOSECONDS` + +- `workQueue`:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择: + + - `ArrayBlockingQueue`:基于数组的先进先出队列,此队列创建时必须指定大小; + - `LinkedBlockingQueue`:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE; + - `SynchronousQueue`:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。 + - `PriorityBlockingQuene`:具有优先级的无界阻塞队列 + +- `threadFactory`:线程工厂,主要用来创建线程 +- `handler`:表示当拒绝处理任务时的策略,有以下四种取值: + + - `ThreadPoolExecutor.AbortPolicy`:丢弃任务并抛出`RejectedExecutionException`异常 + - `ThreadPoolExecutor.DiscardPolicy`:也是丢弃任务,但是不抛出异常 + - `ThreadPoolExecutor.DiscardOldestPolicy`:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) + - `ThreadPoolExecutor.CallerRunsPolicy`:由调用线程处理该任务 + + + +线程池状态 +--- + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_poll_state.png) + + + +`ThreadPoolExecutor`中定义了一个`volatile`变量,另外定义了几个`static final`变量表示线程池的各个状态: + +- `volatile int runState;` 表示当前线程池的状态,它是一个`volatile`变量用来保证线程之间的可见性 +- `static final int RUNNING = -1;`此状态下,线程池可以接受新的任务,也可以处理阻塞队列中的任务。执行shutdown()方法可进入待关闭(SHUTDOWN)状态,执行shutdownNow()方法可进入停止(STOP)状态。 +- `static final int SHUTDOWN = 0;`如果调用了`shutdown()`方法,则线程池处于`SHUTDOWN`状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕 +- `static final int STOP = 1;`如果调用了`shutdownNow()`方法,则线程池处于`STOP`状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务; +- `static final int TIDYING = 2;`此状态下,所有任务都已经执行完毕,且没有工作线程。执行terminated()方法进入终止(TERMINATED)状态。该状态表示线程池对线程进行整理优化; +- `static final int TERMINATED = 3;`当线程池处于`SHUTDOWN`或`STOP`状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为`TERMINATED`状态。此状态下,线程池完全终止,并完成了所有资源的释放。 + + +`Executors`类 +--- + +不过在`java doc`中,并不提倡我们直接使用`ThreadPoolExecutor`,而是使用`Executors`类中提供的几个静态方法来创建线程池: + +- `Executors.newCachedThreadPool();` //创建一个可缓存线程池,缓冲池容量大小为`Integer.MAX_VALUE` +- `Executors.newSingleThreadExecutor();` //创建容量为1的缓冲池,即线程池中每次只有一个线程工作,单线程串行执行任务, +- `Executors.newFixedThreadPool(int);` //创建固定容量大小的缓冲池,固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行 +- `Executors.newScheduleThreadExecutor();`// 大小无限制的线程池,能按时间计划来执行任务,允许用户设定计划执行任务的时间。在实际的业务场景中可以使用该线程池定期的同步数据。 + +这几个方法的具体实现: + +```java +public static ExecutorService newFixedThreadPool(int nThreads) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); +} +public static ExecutorService newSingleThreadExecutor() { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue())); +} +public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue()); +} + +public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + return new ScheduledThreadPoolExecutor(corePoolSize); +} + +``` + +从它们的具体实现来看,它们实际上也是调用了`ThreadPoolExecutor`,只不过参数都已配置好了: + +- `newFixedThreadPool`创建的线程池`corePoolSize`和`maximumPoolSize`值是相等的,它使用的`LinkedBlockingQueue`; +- `newSingleThreadExecutor`将`corePoolSize`和`maximumPoolSize`都设置为1,也使用的`LinkedBlockingQueue`; +- `newCachedThreadPool`将`corePoolSize`设置为0,将`maximumPoolSize`设置为`Integer.MAX_VALUE`,使用的`SynchronousQueue`,也就是说来了任务就创建线程运行,当线程空闲超过`60`秒,就销毁线程。 +- `newScheduledThreadPool`:`maximumPoolSize``Integer.MAX_VALUE`使用`DelayedWorkQueue())`。 + +实际中,如果`Executors`提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置`ThreadPoolExecutor`的参数有点麻烦,要根据实际任务的类型和数量来进行配置。 + +## 线程池执行状态 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_poll_process.png) +整个过程可以拆分成以下几个部分: + +- 提交任务 + + 当向线程池提交一个新的任务时,线程池有三种处理情况,分别是:创建一个工作线程来执行该任务、将任务加入阻塞队列、拒绝该任务。 + + 提交任务的过程也可以拆分成以下几个部分: + + 1. 当工作线程数小于核心线程数时,直接创建新的核心工作线程。 + 2. 当工作线程数大于核心线程数时,就需要尝试将任务添加到阻塞队列中去。 + 3. 如果能够加入成功,说明队列还没满,那么就需要做以下的二次校验来保证添加进去的任务能够成功被执行。 + 4. 验证当前线程池中的运行状态,如果是非RUNNING状态,则需要将任务从阻塞队列中移除,然后拒绝该任务。 + 5. 验证当前线程池中的工作线程的个数,如果是0,则需要主动添加一个空工作线程来执行刚刚添加到阻塞队列中的任务。 + 6. 如果加入失败,说明队列已经满了,这时就需要创建新的临时工作线程来执行任务。 + 7. 如果创建成功,则直接执行该任务。 + 8. 如果创建失败,说明工作线程数已经等于最大线程数了,只能拒绝该任务了。 + +- 创建工作线程 + + 创建工作线程需要做一系列的判断,需要确保当前线程池可以创建新的线程之后,才能创建。 + + 首先,当线程池的状态是SHUTDOWN或者STOP时,不能创建新的线程。 + + 其次,当线程工厂创建线程失败时,也不能创建新的线程。 + + 第三,拿当前工作线程的数量与核心线程数、最大线程数进行比较,如果前者大于后者的话,也不允许创建。除此之外,线程池会尝试通过CAS来自增工作线程的个数,如果自增成功了,则会创建新的工作线程,即Worker对象。 + + 然后加锁进行二次验证是否能够创建工作线程,如果最后创建成功,则会启动该工作线程。 + +- 启动工作线程 + + 当工作线程创建成功后,也就是Worker对象已经创建好了,这时就需要启动该工作线程,让线程开始干活了,Worker对象中关联着一个Thread,所以要启动工作线程的话,只要通过worker.thread.start()来启动该线程即可。 + + 启动完了之后,就会执行Worker对象的run方法,因为Worker实现了Runnable接口,所以本质上Worker也是一个线程。 + + 通过线程start开启之后就会调用到Runnable的run方法,在Worker对象的run方法中,调用了runWorker(this)方法,也就是把当前对象传递给了runWorker()方法,让它来执行。 + +- 获取任务并执行 + + 在runWorker方法被调用之后,就是执行具体的任务了,首先需要拿到一个可以执行的任务,而Worker对象中默认绑定了一个任务,如果该任务不为空的话,那么就是直接执行。 + + 执行完了之后,就会去阻塞队列中获取任务来执行。 + + 获取任务的过程则需要考虑当前工作线程的个数: + + 1. 如果工作线程数大于核心线程数,那么就需要通过poll(keepAliveTime, timeUnit)来获取,因为这时需要对闲置线程进行超时回收。 + 2. 如果工作线程数小于等于核心线程数,那么就可以通过take()来获取了。因为这时所有的线程都是核心线程,不需要进行回收,前提是没有设置allowCoreThreadTimeOut(允许核心线程超时回收)为true。 + + + +向线程池提交任务 +--- + +有两种方式: + +- `Executor.execute(Runnable command);` +- `ExecutorService.submit(Callable task);` + +##### `execute()`内部实现 + +- 首次通过`workCountof()`获知当前线程池中的线程数,如果小于`corePoolSize`, 就通过`addWorker()`创建线程并执行该任务;否则,将该任务放入阻塞队列; +- 如果能成功将任务放入阻塞队列中,如果当前线程池是非RUNNING状态,则将该任务从阻塞队列中移除,然后执行`reject()`处理该任务;如果当前线程池处于RUNNING状态,则需要再次检查线程池(因为可能在上次检查后,有线程资源被释放),是否有空闲的线程;如果有则执行该任务; +- 如果不能将任务放入阻塞队列中,说明阻塞队列已满;那么将通过`addWoker()`尝试创建一个新的线程去执行这个任务;如果`addWoker()`执行失败,说明线程池中线程数达到`maxPoolSize`,则执行`reject()`处理任务; + + +##### `submit()`内部实现 + +会将提交的`Callable`任务会被封装成了一个`FutureTask`对象,`FutureTask`类实现了`Runnable`接口,这样就可以通过`Executor.execute()`提交`FutureTask`到线程池中等待被执行,最终执行的是`FutureTask`的`run`方法; + +比较: + +两个方法都可以向线程池提交任务,`execute()`方法的返回类型是`void`,它定义在`Executor`接口中, 而`submit()`方法可以返回持有计算结果的`Future`对象,它定义在`ExecutorService`接口中,它扩展了`Executor`接口,其它线程池类像`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`都有这些方法。 + +线程池的关闭 +--- + +`ThreadPoolExecutor`提供了两个方法,用于线程池的关闭: + +- `shutdown()`:不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务 +- `shutdownNow()`:立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" "b/JavaKnowledge/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" similarity index 97% rename from "Java\345\237\272\347\241\200/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" rename to "JavaKnowledge/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" index e52cfe12..cc7e9c2c 100644 --- "a/Java\345\237\272\347\241\200/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" +++ "b/JavaKnowledge/\347\275\221\347\273\234\350\257\267\346\261\202\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" @@ -1,216 +1,216 @@ -网络请求相关内容总结 -=== - -网络数据传输,熟悉多线程、Socket网络编程、熟悉TCP、UDP、HTTP等协议 - -- 网络编程概述: - - 网络模型: - - OSI模型 - - 应用层 - - 表示层 - - 会话层 - - 传输层 - - 网络层 - - 数据连接层 - - 物理层 - - TCP/IP模型 - - 应用层 - - 传输层 - - 网际层 - - 主机至网络层 - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/TCP-IP-model-vs-OSI-model.png?raw=true) - - - 网络通讯要素 - - IP地址 - - 端口号 - - 传输协议 - - 网络通讯前提: - - 找到对方IP - - 数据要发送到指定端口。为了标示不同的应用程序,所以给这些网络应用程序都用数字进行标示这个表示就叫端口 - - 定义通信规则。这个规则称为通信协议,国际组织定义了通用协议TCP/IP - -- TCP和UDP的区别: - - UDP协议: - 面向无连接 - 每个数据报的大小在限制在64k内 - 因为是面向无连接,所以是不可靠协议 - 不需要建立连接,速度快 - - TCP协议: - 必须建立连接,形成传输数据的通道 - 在连接中可进行大数据量传输 - 通过三次握手完成连接,是可靠协议 - 必须建立连接,效率会稍低 - - 三次握手: - - 第一次:我问你:在么? 建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。 - - 第二次:你回答:在。服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。 - - 第三次:我反馈:哦,我知道你在了。客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。 - - 四次挥手: - - 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。 - - 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。 - - 服务器B关闭与客户端A的连接,发送一个FIN给客户端A。 - - 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。 - -- Socket: - - Socket是对TCP/IP协议的封装和应用。Socket本身并不是协议,而是一个调用接口。通过Socket,我们才能使用TCP/IP协议.在设计模式中Socket其实就是一个门面模式。 - 他将复杂的TCP/IP协议归隐到Socket接口后面,对于使用来说,一组简单的接口就是全部,让Socket去组织符合制定协议的数据。 - - Socket就是为网络服务提供的一种机制 - - 通信的两端都有Socket - - 网络通信其实就是Socket间的通信 - - 数据在两个Socket间通过IO传输 - - 玩Socket主要就是记住流程,代码查文档就行 - -- UDP(User Datagram Protocol):用户数据协议 - - UDP概述: - 需要DatagramSocket与DatagramPacket对象来实现UDP协议传输数据 - UDP协议是一种面向无连接的协议。面向无连接的协议指的是正式通信前不必与对方先建立连接,不管对方连接状态就直接发送数据。 - - UDP协议开发步骤: - - 发送端: - - 建立DatagramSocket服务; - - 提供数据,并将数据封装到字节数组中; - - 创建DatagramPacket数据包,并把数据封装到包中,同时指定接收端IP和接收端口 - - 通过Socket服务,利用send方法将数据包发送出去; - - 关闭DatagramSocket和DatagramPacket服务。 - - 接收端: - - 建立DatagramSocket服务,并监听一个端口; - - 定义一个字节数组和一个数据包,同时将数组封装进数据包; - - DatagramPacket的receive方法,将接收的数据存入定义好的数据包; - - 通过DatagramPacke关闭的方法,获取发送数据包中的信息; - - 关闭DatagramSocket和DatagramPacket服务。 - - UDP协议的Demo(必须掌握): - - 发送端: - - ```java - class UDPSend { - public static void main(String[] args) throws Exception { - DatagramSocket ds = new DatagramSocket(); - byte[] buf = "这是UDP发送端".getBytes(); - DatagramPacket dp = new DatagramPacket( - buf,buf.length,InetAddress.getByName("192.168.1.253"),10000); - ds.send(dp); - ds.close(); - } - } - ``` - - 接收端 - - ```java - class UDPRece { - public static void main(String[] args) throws Exception { - DatagramSocket ds = new DatagramSocket(10000); - byte[] buf = new byte[1024]; - DatagramPacket dp = new DatagramPacket(buf,buf.length); - ds.receive(dp);//将发送端发送的数据包接收到接收端的数据包中 - String ip = dp.getAddress().getHosyAddress();//获取发送端的ip - String data = new String(dp.getData(),0,dp.getLength());//获取数据 - int port = dp.getPort();//获取发送端的端口号 - sop(ip+":"+data+":"+port); - ds.close(); - } - } - ``` -- TCP/IP协议:Socket和ServerSocket - - 基于TCP协议的网络通信概述: - - TCP/IP通信协议是一种必须建立连接的可靠的网络通信协议。它在通信两端各建立一个Socket,从而在通信的两端之间形成网络虚拟链路。 - - 网络虚拟链路一旦建立,两端的程序就可以进行通信。 - - TCP/IP协议开发步骤: - - 客户端: - - 建立Socket服务,并指定要连接的主机和端口; - - 获取Socket流中的输出流OutputStream,将数据写入流中,通过网络发送给服务端; - - 获取Socket流中的输出流InputStream,获取服务端的反馈信息; - - 关闭资源。 - - 服务端: - - 建立ServerSocket服务,并监听一个端口; - - 通过ServerSocket服务的accept方法,获取Socket服务对象; - - 使用客户端对象的读取流获取客户端发送过来的数据; - - 通过客户端对象的写入流反馈信息给客户端; - - 关闭资源 - - - TCP/IP协议的一个Demo(必须要掌握!): - - 客户端: - - ```java - class TCPClient { - public static void main(String[] args) { - Socket s = new Socket("192.168.1.253",10000); - OutputStream os = s.getOutputStream(); - out.write("这是TCP发送的数据".getBytes()); - s.close(); - } - } - ``` - - 服务端: - - ```java - class TCPServer { - public static void main(String[] args) { - ServerSocket ss = new ServerSocket(10000); - Socket s = ss.accept(); - - String ip = s.getInetAddress().getHostAddress(); - sop(ip); - - InputStream is = s.getInputStream(); - byte[] buf = new byte[1024]; - int len = is.read(buf); - sop(new String(buf,0,len)); - s.close(); - ss.close(); - } - } - ``` - -- HTTP协议: - - HTTP是Hyper Text Transfer Protocol的缩写 - - 是由W3C制定和维护的。目前版本为1.0和1.1 - - 是开发web的基石,非常地重要 - - Http虽然本身是一个协议,但是最终还是基于TCP的。 - - 首先由客户建立一条与服务器的TCP连接,然后发送一个请求到服务器,服务器向再进行响应。一次TCP连接的建立将需要3次握手。 - - 版本 - - 1.0版本:是无状态的协议,即一次连接只响应一次请求,响应完了就关闭此次连接要想再访问须重新建立连接。而连接都是比较耗资源的。 - - 1.1版本:是有状态的协议。即可以在一次网络连接基础上发出多次请求和得到多次的响应。当距离上次请求时间过长时,服务器会自动断掉连接,这就是超时机制。 - - HTTP协议的组成: - - 请求部分: - - 请求行: - - GET / HTTP/1.1 包含:请求方式GET 请求的资源路径:/ 协议版本号:HTTP/1.1 - - 请求方式。常用的有GET、POST - - GET方式:默认方式。直接输入的网址。 - - 表单数据出现在请求行中。url?username=abc&password=123 - - 特点:不安全;有长度限制:<1k - - POST方式:可以通过表单form method="post"设置 - - 表单数据会出现在正文中。 - - 特点:安全;没有长度限制 - - 请求消息头: - - 请求正文:第一个空行之后的全部都是请求正文 - - 响应部分: - - 响应行: - - HTTP/1.1 200 OK 包含:协议版本号:HTTP/1.1 响应码:200 描述:OK - - 响应码:(实际用到的30个左右,其他都是W3C保留的) - - 描述:对响应码的描述 - - 常用响应码: - - 200:一切正常 - - 302/307:请求的资源路径变更了 - - 304:资源没有被修改过 - - 404:资源不存在,找不到资源 - - 500:服务器程序有错 - - 响应消息头: - - 响应正文: - - 第一个空行之后的全部都是响应正文,浏览器显示的就是正文中的内容 - -- HTTPS - HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议 - 它是一个安全通信通道,它基于HTTP开发,用于在客户计算机和服务器之间交换信息。它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版。 - -- HTTPS和HTTP的区别: - - https协议需要到ca申请证书,一般免费证书很少,需要交费。 - - http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议 - - http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。 - - http的连接很简单,是无状态的 - - HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全 - ----- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - +网络请求相关内容总结 +=== + +网络数据传输,熟悉多线程、Socket网络编程、熟悉TCP、UDP、HTTP等协议 + +- 网络编程概述: + - 网络模型: + - OSI模型 + - 应用层 + - 表示层 + - 会话层 + - 传输层 + - 网络层 + - 数据连接层 + - 物理层 + - TCP/IP模型 + - 应用层 + - 传输层 + - 网际层 + - 主机至网络层 + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/TCP-IP-model-vs-OSI-model.png?raw=true) + + - 网络通讯要素 + - IP地址 + - 端口号 + - 传输协议 + - 网络通讯前提: + - 找到对方IP + - 数据要发送到指定端口。为了标示不同的应用程序,所以给这些网络应用程序都用数字进行标示这个表示就叫端口 + - 定义通信规则。这个规则称为通信协议,国际组织定义了通用协议TCP/IP + +- TCP和UDP的区别: + - UDP协议: + 面向无连接 + 每个数据报的大小在限制在64k内 + 因为是面向无连接,所以是不可靠协议 + 不需要建立连接,速度快 + - TCP协议: + 必须建立连接,形成传输数据的通道 + 在连接中可进行大数据量传输 + 通过三次握手完成连接,是可靠协议 + 必须建立连接,效率会稍低 + + 三次握手: + - 第一次:我问你:在么? 建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。 + - 第二次:你回答:在。服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。 + - 第三次:我反馈:哦,我知道你在了。客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。 + + 四次挥手: + - 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。 + - 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。 + - 服务器B关闭与客户端A的连接,发送一个FIN给客户端A。 + - 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。 + +- Socket: + - Socket是对TCP/IP协议的封装和应用。Socket本身并不是协议,而是一个调用接口。通过Socket,我们才能使用TCP/IP协议.在设计模式中Socket其实就是一个门面模式。 + 他将复杂的TCP/IP协议归隐到Socket接口后面,对于使用来说,一组简单的接口就是全部,让Socket去组织符合制定协议的数据。 + - Socket就是为网络服务提供的一种机制 + - 通信的两端都有Socket + - 网络通信其实就是Socket间的通信 + - 数据在两个Socket间通过IO传输 + - 玩Socket主要就是记住流程,代码查文档就行 + +- UDP(User Datagram Protocol):用户数据协议 + - UDP概述: + 需要DatagramSocket与DatagramPacket对象来实现UDP协议传输数据 + UDP协议是一种面向无连接的协议。面向无连接的协议指的是正式通信前不必与对方先建立连接,不管对方连接状态就直接发送数据。 + - UDP协议开发步骤: + - 发送端: + - 建立DatagramSocket服务; + - 提供数据,并将数据封装到字节数组中; + - 创建DatagramPacket数据包,并把数据封装到包中,同时指定接收端IP和接收端口 + - 通过Socket服务,利用send方法将数据包发送出去; + - 关闭DatagramSocket和DatagramPacket服务。 + - 接收端: + - 建立DatagramSocket服务,并监听一个端口; + - 定义一个字节数组和一个数据包,同时将数组封装进数据包; + - DatagramPacket的receive方法,将接收的数据存入定义好的数据包; + - 通过DatagramPacke关闭的方法,获取发送数据包中的信息; + - 关闭DatagramSocket和DatagramPacket服务。 + - UDP协议的Demo(必须掌握): + - 发送端: + + ```java + class UDPSend { + public static void main(String[] args) throws Exception { + DatagramSocket ds = new DatagramSocket(); + byte[] buf = "这是UDP发送端".getBytes(); + DatagramPacket dp = new DatagramPacket( + buf,buf.length,InetAddress.getByName("192.168.1.253"),10000); + ds.send(dp); + ds.close(); + } + } + ``` + - 接收端 + + ```java + class UDPRece { + public static void main(String[] args) throws Exception { + DatagramSocket ds = new DatagramSocket(10000); + byte[] buf = new byte[1024]; + DatagramPacket dp = new DatagramPacket(buf,buf.length); + ds.receive(dp);//将发送端发送的数据包接收到接收端的数据包中 + String ip = dp.getAddress().getHosyAddress();//获取发送端的ip + String data = new String(dp.getData(),0,dp.getLength());//获取数据 + int port = dp.getPort();//获取发送端的端口号 + sop(ip+":"+data+":"+port); + ds.close(); + } + } + ``` +- TCP/IP协议:Socket和ServerSocket + - 基于TCP协议的网络通信概述: + - TCP/IP通信协议是一种必须建立连接的可靠的网络通信协议。它在通信两端各建立一个Socket,从而在通信的两端之间形成网络虚拟链路。 + - 网络虚拟链路一旦建立,两端的程序就可以进行通信。 + - TCP/IP协议开发步骤: + - 客户端: + - 建立Socket服务,并指定要连接的主机和端口; + - 获取Socket流中的输出流OutputStream,将数据写入流中,通过网络发送给服务端; + - 获取Socket流中的输出流InputStream,获取服务端的反馈信息; + - 关闭资源。 + - 服务端: + - 建立ServerSocket服务,并监听一个端口; + - 通过ServerSocket服务的accept方法,获取Socket服务对象; + - 使用客户端对象的读取流获取客户端发送过来的数据; + - 通过客户端对象的写入流反馈信息给客户端; + - 关闭资源 + + - TCP/IP协议的一个Demo(必须要掌握!): + - 客户端: + + ```java + class TCPClient { + public static void main(String[] args) { + Socket s = new Socket("192.168.1.253",10000); + OutputStream os = s.getOutputStream(); + out.write("这是TCP发送的数据".getBytes()); + s.close(); + } + } + ``` + - 服务端: + + ```java + class TCPServer { + public static void main(String[] args) { + ServerSocket ss = new ServerSocket(10000); + Socket s = ss.accept(); + + String ip = s.getInetAddress().getHostAddress(); + sop(ip); + + InputStream is = s.getInputStream(); + byte[] buf = new byte[1024]; + int len = is.read(buf); + sop(new String(buf,0,len)); + s.close(); + ss.close(); + } + } + ``` + +- HTTP协议: + - HTTP是Hyper Text Transfer Protocol的缩写 + - 是由W3C制定和维护的。目前版本为1.0和1.1 + - 是开发web的基石,非常地重要 + - Http虽然本身是一个协议,但是最终还是基于TCP的。 + - 首先由客户建立一条与服务器的TCP连接,然后发送一个请求到服务器,服务器向再进行响应。一次TCP连接的建立将需要3次握手。 + - 版本 + - 1.0版本:是无状态的协议,即一次连接只响应一次请求,响应完了就关闭此次连接要想再访问须重新建立连接。而连接都是比较耗资源的。 + - 1.1版本:是有状态的协议。即可以在一次网络连接基础上发出多次请求和得到多次的响应。当距离上次请求时间过长时,服务器会自动断掉连接,这就是超时机制。 + - HTTP协议的组成: + - 请求部分: + - 请求行: + - GET / HTTP/1.1 包含:请求方式GET 请求的资源路径:/ 协议版本号:HTTP/1.1 + - 请求方式。常用的有GET、POST + - GET方式:默认方式。直接输入的网址。 + - 表单数据出现在请求行中。url?username=abc&password=123 + - 特点:不安全;有长度限制:<1k + - POST方式:可以通过表单form method="post"设置 + - 表单数据会出现在正文中。 + - 特点:安全;没有长度限制 + - 请求消息头: + - 请求正文:第一个空行之后的全部都是请求正文 + - 响应部分: + - 响应行: + - HTTP/1.1 200 OK 包含:协议版本号:HTTP/1.1 响应码:200 描述:OK + - 响应码:(实际用到的30个左右,其他都是W3C保留的) + - 描述:对响应码的描述 + - 常用响应码: + - 200:一切正常 + - 302/307:请求的资源路径变更了 + - 304:资源没有被修改过 + - 404:资源不存在,找不到资源 + - 500:服务器程序有错 + - 响应消息头: + - 响应正文: + - 第一个空行之后的全部都是响应正文,浏览器显示的就是正文中的内容 + +- HTTPS + HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议 + 它是一个安全通信通道,它基于HTTP开发,用于在客户计算机和服务器之间交换信息。它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版。 + +- HTTPS和HTTP的区别: + - https协议需要到证书颁发机构(Certificate Authority,简称CA)申请证书,一般免费证书很少,需要交费。 + - http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议 + - http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。 + - http的连接很简单,是无状态的 + - HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全 + +---- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/\350\216\267\345\217\226\344\273\212\345\220\216\345\244\232\345\260\221\345\244\251\345\220\216\347\232\204\346\227\245\346\234\237.md" "b/JavaKnowledge/\350\216\267\345\217\226\344\273\212\345\220\216\345\244\232\345\260\221\345\244\251\345\220\216\347\232\204\346\227\245\346\234\237.md" similarity index 96% rename from "Java\345\237\272\347\241\200/\350\216\267\345\217\226\344\273\212\345\220\216\345\244\232\345\260\221\345\244\251\345\220\216\347\232\204\346\227\245\346\234\237.md" rename to "JavaKnowledge/\350\216\267\345\217\226\344\273\212\345\220\216\345\244\232\345\260\221\345\244\251\345\220\216\347\232\204\346\227\245\346\234\237.md" index 02e276ed..773bd565 100644 --- "a/Java\345\237\272\347\241\200/\350\216\267\345\217\226\344\273\212\345\220\216\345\244\232\345\260\221\345\244\251\345\220\216\347\232\204\346\227\245\346\234\237.md" +++ "b/JavaKnowledge/\350\216\267\345\217\226\344\273\212\345\220\216\345\244\232\345\260\221\345\244\251\345\220\216\347\232\204\346\227\245\346\234\237.md" @@ -1,35 +1,35 @@ -获取今后多少天后的日期 -=== - -```java -/** - * Get the date some days later. - * @param year the year - * @param month month of the year - * @param day day of the month - * @return if the parameter is illegal this will return null - */ -@SuppressLint("SimpleDateFormat") -private static String getClosingDate(int year, int month, int day) { - final int internalDay = 31; - final String pattern = "yyyy-MM-dd"; - DateFormat dateFormat = new SimpleDateFormat(pattern); - Date closingDate; - try { - Calendar thisDay = Calendar.getInstance(); - thisDay.set(Calendar.YEAR, year); - thisDay.set(Calendar.MONTH, month - 1);// the first month of the year is 0. - thisDay.set(Calendar.DAY_OF_MONTH, day); - thisDay.add(Calendar.DAY_OF_MONTH, internalDay); - closingDate = thisDay.getTime(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - return dateFormat.format(closingDate); -} -``` ---- - -- 邮箱 :charon.chui@gmail.com +获取今后多少天后的日期 +=== + +```java +/** + * Get the date some days later. + * @param year the year + * @param month month of the year + * @param day day of the month + * @return if the parameter is illegal this will return null + */ +@SuppressLint("SimpleDateFormat") +private static String getClosingDate(int year, int month, int day) { + final int internalDay = 31; + final String pattern = "yyyy-MM-dd"; + DateFormat dateFormat = new SimpleDateFormat(pattern); + Date closingDate; + try { + Calendar thisDay = Calendar.getInstance(); + thisDay.set(Calendar.YEAR, year); + thisDay.set(Calendar.MONTH, month - 1);// the first month of the year is 0. + thisDay.set(Calendar.DAY_OF_MONTH, day); + thisDay.add(Calendar.DAY_OF_MONTH, internalDay); + closingDate = thisDay.getTime(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return dateFormat.format(closingDate); +} +``` +--- + +- 邮箱 :charon.chui@gmail.com - Good Luck! \ No newline at end of file diff --git "a/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" new file mode 100644 index 00000000..c0551d48 --- /dev/null +++ "b/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -0,0 +1,2034 @@ +设计模式(Design Patterns) +=== + +设计模式`(Design pattern)`是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他 +人理解、保证代码可靠性。 +毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。 +项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及 +该问题的核心解决方案,这也是它能被广泛应用的原因。 + + +设计模式的分类 +--- + +总体来说设计模式分为三类: + +- 创建型模式:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 +- 结构型模式:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 +- 行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 + + +设计模式的六大原则 +--- + +- 开闭原则`(Open Close Principle)` + 开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是: + 为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 + 当系统升级时,如果为了增强系统功能而需要进行大量的代码修改,则说明这个系统的设计是失败的,是违反开闭原则的。反之,对系统的扩展应该只需添加 + 新的软件模块,系统模式一旦确立就不再修改现有代码,这才是符合开闭原则的优雅设计。其实开闭原则在各种设计模式中都有体现,对抽象的大量运用奠定 + 了系统可复用性、可扩展性的基础,也增加了系统的稳定性。 + +- 里氏代换原则`(Liskov Substitution Principle)` + 里氏代换原则面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 `LSP`是继承复用的基石,只有当衍生类 + 可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 + 里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实 + 现抽象化的具体步骤的规范。 + +- 依赖倒转原则`(Dependence Inversion Principle)` + 我们知道,面向对象中的依赖是类与类之间的一种关系,如H(高层)类要调用L(底层)类的方法,我们就说H类依赖L类。 + 依赖倒置原则指高层模块不依赖底层模块,也就是说高层模块只依赖上层抽象,而不直接依赖具体的底层实现,从而达到降低耦合的目的。如上面提到的H与L + 的依赖关系必然会导致它们的强耦合,也许L任何细枝末节的变动都可能影响H,这是一种非常死板的设计。而依赖倒置的做法则是反其道而行,我们可以创建 + L的上层抽象A,然后H即可通过抽象A间接地访问L,那么高层H不再依赖底层L,而只依赖上层抽象A。这样一来系统会变得更加松散,这也印证了我们在 + “里氏替换原则”中所提到的“面向接口编程”,以达到替换底层实现的目的。 + 举个例子,公司总经理制订了下一年度的目标与计划,为了提高办公效率,总经理决定年底要上线一套全新的办公自动化软件。那么总经理作为发起方该如何 + 实施这个计划呢?直接发动基层程序员并调用他们的研发方法吗?我想世界上没有以这种方式管理公司的领导吧。公司高层一定会发动IT部门的上层抽象去 + 执行,调用IT部门经理的work方法并传入目标即可,至于这个work方法的具体实现者也许是架构师甲,也可能是程序员乙,总经理也许根本不认识他们, + 这就达到了公司高层与底层员工实现解耦的目的。这就是将“高层依赖底层”倒置为“底层依赖高层”的好处。 + +- 接口隔离原则`(Interface Segregation Principle)` + 接口隔离原则指的是对高层接口的独立、分化,客户端对类的依赖基于最小接口,而不依赖不需要的接口。简单来说,就是切勿将接口定义成全能型的,否则 + 实现类就必须神通广大,这样便丧失了子类实现的灵活性,降低了系统的向下兼容性。反之,定义接口的时候应该尽量拆分成较小的粒度,往往一个接口只对 + 应一个职能。 + + 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计 + 思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 + +- 迪米特法则(最少知识原则)`(Demeter Principle)` + 迪米特法则也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、 + 简单化模块间的通信,并达到松耦合的目的。反之,模块之间若存在过多的关联,那么一个很小的变动则可能会引发蝴蝶效应般的连锁反应,最终会波及大范 + 围的系统变动。我们说,缺乏良好封装性的系统模块是违反迪米特法则的,牵一发动全身的设计使系统的扩展与维护变的举步维艰。举个例子,我们买了一台 + 游戏机,主机内部集成了非常复杂的电路及电子元件,这些对外部来说完全是不可见的,就像一个黑盒子。虽然我们看不到黑盒子的内部构造与工作原理, + 但它向外部开放了控制接口,让我们可以接上手柄对其进行访问,这便构成了一个完美的封装。 + “门面模式”就是极好的范例。例如我们去某单位办理一项业务,来到业务大厅一脸茫然,各种填表、盖章等复杂的办理流程让人一头雾水,有可能来回折腾 + 几个小时。假若有一个提供快速通道服务的“门面”办理窗口,那么我们只需简单地把材料递交过去就可以了,“办理人“与“门面”保持最简单的通信,对于门面 + 里面发生的事情,办理人则知之甚少,更没有必要去亲力亲为。要设计出符合迪米特法则的软件,切勿跨越红线,干涉他人内务。系统模块一定要最大程度地 + 隐藏内部逻辑,大门一定要紧锁,防止陌生人随意访问,而对外只适可而止地暴露最简单的接口,让模块间的通信趋向“简单化”“傻瓜化”。 + + +- 合成复用原则`(Composite Reuse Principle)` + 原则是尽量使用合成/聚合的方式,而不是使用继承。 + +- 单一职责原则(Single Responsibility Principle) + 我们知道,一套功能完备的软件系统可能是非常复杂的。既然要利用好面向对象的思想,那么对一个大系统的拆分、模块化是不可或缺的软件设计步骤。 + 面向对象以“类”来划分模块边界,再以“方法”来分隔其功能。我们可以将某业务功能划归到一个类中,也可以拆分为几个类分别实现,但是不管对其负责的业 + 务范围大小做怎样的权衡与调整,这个类的角色职责应该是单一的,或者其方法所完成的功能也应该是单一的。总之,不是自己分内之事绝不该负责,这就是 + 单一职责原则。 + 以最典型的“责任链模式”为例,其环环相扣的每个节点都“各扫门前雪”,这种清晰的职责范围划分就是单一职责原则的最佳实践。符合单一职责原则的设计 + 能使类具备“高内聚性”,让单个模块变得“简单”“易懂”,如此才能增强代码的可读性与可复用性,并提高系统的易维护性与易测试性。 + + +1.工厂方法模式`(Factory Method)` +--- + +制造业是一个国家工业经济发展的重要支柱,而工厂则是其根基所在。程序设计中的工厂类往往是对对象构造、实例化、初始化过程的封装,而工厂方法则可以升华 +为一种设计模式,它对工厂制造方法进行接口规范化,以允许子类工厂决定具体制造哪类产品的实例,最终降低系统耦合,使系统的可维护性、可扩展性等得到提升。 +工厂内部封装的生产逻辑对外部来说像一个黑盒子,外部不需要关心工厂内部细节,外部类只管调用即可。 + +工厂方法模式分为三种: + +- 普通工厂模式:就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。 + + 这里举一个发送邮件和发送短信的例子. + + 首先,创建两者的共同接口: + ```java + public interface ISender { + void send(); + } + ``` + + 然后分别创建具体的实现类: + ```java + public class MailSender implements ISender { + @Override + public void send() { + System.out.println("mail sender"); + } + } + + public class SmsSender implements ISender { + @Override + public void send() { + System.out.println("sms sender"); + } + } + ``` + 最后是建立工厂类: + ```java + public class SendFactory { + public ISender produce(String type) { + if ("mail".equals(type)) { + return new MailSender (); + } else if ("sms".equals(type)) { + return new SmsSender (); + } else { + System.out.println("请输入正确的类型!"); + return null; + } + } + } + ``` + +- 多个工厂方法模式:是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。 + + 将上面的代码进行修改,主要修改`SendFactory`就行: + ```java + public class SendFactory { + public ISender produceMail(){ + return new MailSender(); + } + + public ISender produceSms(){ + return new SmsSender(); + } + } + ``` + +- 静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。 + + ```java + public class SendFactory { + public static ISender produceMail(){ + return new MailSender(); + } + + public static ISender produceSms(){ + return new SmsSender(); + } + } + ``` + + 测试类: + ```java + ISender sender = SendFactory.produceMail(); + sender.Send(); + ``` + +总体来说,工厂模式适合凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中, +第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。 + +2.抽象工厂模式`(Abstract Factory)` +--- + +工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑, +有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。 + +抽象工厂模式是对工厂的抽象化,而不只是制造方法。我们知道,为了满足不同用户对产品的多样化需求,工厂不会只局限于生产一类产品,但是系统如果按工厂 +方法那样为每种产品都增加一个新工厂又会造成工厂泛滥。所以,为了调和这种矛盾,抽象工厂模式提供了另一种思路,将各种产品分门别类,基于此来规划各种 +工厂的制造接口,最终确立产品制造的顶级规范,使其与具体产品彻底脱钩。 +抽象工厂是建立在制造复杂产品体系需求基础之上的一种设计模式,在某种意义上,我们可以将抽象工厂模式理解为工厂方法模式的高度集群化升级版。 +针对这种情况,我们就需要进行产业规划与整合,对现有工厂进行重构。例如,我们可以基于产品品牌与系列进行生产线规划,按品牌划分A工厂与B工厂。 +具体以汽车工厂举例,A品牌汽车有轿车、越野车、跑车3个系列的产品,同样的,B品牌汽车也包括以上3个系列的产品,如此便形成了两个产品族,分别由A工厂 +和B工厂负责生产,每个工厂都有3条生产线,分别生产这3个系列的汽车。 + +还是用上面的例子,只是在工厂类这里需要改一下,提供两个不同的工厂类,他们要实现同一个接口: + +```java +public interface IProvider { + ISender produce(); +} +``` + +具体两个工厂类的实现: + +```java +public class SendMailFactory implements IProvider { + @Override + public ISender produce(){ + return new MailSender(); + } +} + +public class SendSmsFactory implements IProvider{ + @Override + public ISender produce() { + return new SmsSender(); + } +} +``` + +测试类: + +```java +IProvider provider = new SendMailFactory(); +ISender sender = provider.produce(); +sender.Send(); +``` + +其实这个模式的好处就是,如果你现在想增加一个功能:发送即时信息,则只需做一个实现类,实现`ISender`接口,同时做一个工厂类,实现`IProvider`接口, +就`OK`了,无需去改动现成的代码。这样做,拓展性较好! + + +3.单例模式`(Singleton)` +--- + +单例模式能保证在一个`JVM`中,该对象只有一个实例存在。 + +这样的模式有几个好处: + +- 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。 +- 省去了`new`操作符,降低了系统内存的使用频率,减轻`GC`压力。 +- 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。 + +首先我们写一个简单的单例类: +```java +public class SingleTon { + private static SingleTon instance = null; + private SingleTon() { + } + public static SingleTon getInstance() { + if (instance == null) { + instance = new SingleTon(); + } + return instance; + } + + /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ + public Object readResolve() { + return instance; + } +} +``` + +这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对 +`getInstance()`方法加`synchronized`关键字,但是,`synchronized`关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用 +`getInstance()`,都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个: + +```java +public class SingleTon { + private static SingleTon instance = null; + + private SingleTon() { + } + + public static SingleTon getInstance() { + if (instance == null) { + synchronized (instance) { + if (instance == null) { + instance = new SingleTon(); + } + } + } + return instance; + } + + + /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ + public Object readResolve() { + return instance; + } +} +``` + +似乎解决了之前提到的问题,将`synchronized`关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在`instance`为`null`,并创建对象的时候 +才需要加锁,性能有一定的提升。 +但是,这样的情况,还是有可能有问题的,看下面的情况:在`Java`指令中创建对象和赋值操作是分开进行的,也就是说`instance = new Singleton();` +语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能`JVM`会为新`的Singleton`实例分配空间,然后直接赋值给`instance`成员, +然后再去初始化这个`Singleton`实例。这样就可能出错了。 + +我们以`A`、`B`两个线程为例: + +- `A、B`线程同时进入了第一个`if`判断 +- `A`首先进入`synchronized`块,由于`instance`为`null`,所以它执行`instance = new Singleton();` +- 由于`JVM`内部的优化机制,`JVM`先画出了一些分配给`Singleton`实例的空白内存,并赋值给`instance`成员(注意此时`JVM`没有开始初始化这个实例),然后`A`离开了`synchronized`块。 +- `B`进入`synchronized`块,由于`instance`此时不是`null`,因此它马上离开了`synchronized`块并将结果返回给调用该方法的程序。 +- 此时`B`线程打算使用`Singleton`实例,却发现它没有被初始化,于是错误发生了。 + +所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化: + +```java +public class SingleTon { + + private SingleTon() { + } + + private static class SingletonFactory { + private static SingleTon instance = new SingleTon(); + } + + public static SingleTon getInstance() { + return SingletonFactory.instance; + } + + public Object readResolve() { + return getInstance(); + } +} +``` + +单例模式使用内部类来维护单例的实现,`JVM`内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。 +这样当我们第一次调用`getInstance()`的时候,`JVM`能够帮我们保证`instance`只被创建一次,并且会保证把赋值给`instance`的内存初始化完毕, +这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。 +其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况, +选择最适合自己应用场景的实现方法。 + + +4.建造者模式`(Builder)` +--- + + +建造者模式所构建的对象一定是庞大而复杂的,并且一定是按照既定的制造工序将组件组装起来的,例如计算机、汽车、建筑物等。我们通常将负责构建这些大型 +对象的工程师称为建造者。 +建造者模式又称为生成器模式,主要用于对复杂对象的构建、初始化,它可以将多个简单的组件对象按顺序一步步组装起来,最终构建成一个复杂的成品对象。 +与工厂系列模式不同的是,建造者模式的主要目的在于把烦琐的构建过程从不同对象中抽离出来,使其脱离并独立于产品类与工厂类,最终实现用同一套标准的制造 +工序能够产出不同的产品。 + +工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性, +其实建造者模式就是前面抽象工厂模式和最后的`Test`测试类结合起来得到的。 + +还是前面的例子,一个`ISender`接口,两个实现类`MailSender`和`SmsSender`。最后,建造者类如下: +```java +public class SenderBuilder { + private List list = new ArrayList<>(); + public void produceMailSender(int count) { + for (int i = 0; i < count; i++) { + list.add(new MailSender()); + } + } + + public void produceSmsSender(int count) { + for (int i = 0; i < count; i++) { + list.add(new SmsSender()); + } + } +} +``` + +测试类: +```java +Builder builder = new Builder(); +builder.produceMailSender(10); +``` + +建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工程模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注 +创建符合对象,多个部分。因此,是选择工厂模式还是建造者模式,依实际情况而定。 + +5.原型模式`(Prototype)` +--- + +在制造业中通常是指大批量生产开始之前研发出的概念模型,并基于各种参数指标对其进行检验,如果达到了质量要求,即可参照这个原型进行批量生产。 +原型模式达到以原型实例创建副本实例的目的即可,并不需要知道其原始类。 +也就是说,原型模式可以用对象创建对象,而不是用类创建对象,以此达到效率的提升。 + +构造一个对象的过程是耗时耗力的。想必大家一定有过打印和复印的经历,为了节省成本,我们通常会用打印机把电子文档打印到A4纸上(原型实例化过程), +再用复印机把这份纸质文稿复制多份(原型拷贝过程),这样既实惠又高效。 +那么,对于第一份打印出来的原文稿,我们可以称之为“原型文件”,而对于复印过程,我们则可以称之为“原型拷贝” + +想必大家已经明白了类的实例化与克隆之间的区别,二者都是在造对象,但方法绝对是不同的。 +***原型模式的目的是从原型实例克隆出新的实例,对于那些有非常复杂的初始化过程的对象或者是需要耗费大量资源的情况,原型模式是更好的选择*** + +该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。在`Java`中,复制对象是通过`clone()`实现的,先创建一个原型类: + +```java +public class Prototype implements Cloneable { + public Object clone() throws CloneNotSupportedException { + Prototype proto = (Prototype) super.clone(); + return proto; + } +} +``` + +很简单,一个原型类,只需要实现`Cloneable`接口,重写`clone()`方法,此处`clone()`方法可以改成任意的名称,因为`Cloneable`接口是个空接口, +你可以任意定义实现类的方法名,如`cloneA`或者`cloneB`,因为此处的重点是`super.clone()`这句话, +`super.clone()`调用的是`Object`的`clone()`方法,而在`Object`类中,`clone()`是`native`的。 + +我们都知道,Java中的变量分为原始类型和引用类型,所谓浅拷贝是指只复制原始类型的值,比如横坐标x与纵坐标y这种以原始类型int定义的值, +它们会被复制到新克隆出的对象中。 +而引用类型同样会被拷贝,但是请注意这个操作只是拷贝了地址引用(指针),也就是说副本与原型中的对象是同一个,因为两个同样的地址实际指向的内存对象是 +同一个对象。需要注意的是,克隆方法中调用父类Object的clone方法进行的是浅拷贝,所以此处的bullet并没有被真正克隆。 + +在这儿,将结合对象的浅复制和深复制来说一下,首先需要了解对象深、浅复制的概念: + +- 浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。 +- 深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。 + +此处,写一个深浅复制的例子: + +```java +public class Prototype implements Cloneable, Serializable { + private static final long serialVersionUID = 1L; + private String string; + private SerializableObject obj; + + /* 浅复制 */ + public Object clone() throws CloneNotSupportedException { + Prototype proto = (Prototype) super.clone(); + return proto; + } + + /* 深复制 */ + public Object deepClone() throws IOException, ClassNotFoundException { + /* 写入当前对象的二进制流 */ + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(this); + + /* 读出二进制流产生的新对象 */ + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bis); + return ois.readObject(); + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + + public SerializableObject getObj() { + return obj; + } + + public void setObj(SerializableObject obj) { + this.obj = obj; + } +} + +class SerializableObject implements Serializable { + private static final long serialVersionUID = 1L; +} +``` + +实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。 + +从类到对象叫作“创建”,而由本体对象至副本对象则叫作“克隆”,当需要创建多个类似的复杂对象时,我们就可以考虑用原型模式。究其本质,克隆操作时Java +虚拟机会进行内存操作,直接拷贝原型对象数据流生成新的副本对象,绝不会拖泥带水地触发一些多余的复杂操作(如类加载、实例化、初始化等),所以其效率 +远远高于“new”关键字所触发的实例化操作。看尽世间烦扰,拨开云雾见青天,有时候“简单粗暴”也是一种去繁从简、不绕弯路的解决方案。 + + +适配器模式`(Adapter)` +--- + +适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。 +适配器模式(Adapter)通常也被称为转换器,顾名思义,它一定是进行适应与匹配工作的物件。当一个对象或类的接口不能匹配用户所期待的接口时, +适配器就充当中间转换的角色,以达到兼容用户接口的目的,同时适配器也实现了客户端与接口的解耦,提高了组件的可复用性。 + + +主要分为三类: + +- 类的适配器模式 + + 核心思想就是:有一个`Source`类,拥有一个方法,待适配,目标接口是`Targetable`,通过`Adapter`类,将`Source`的功能扩展到`Targetable`中。 + + ```java + public interface Targetable { + /* 与原类中的方法相同 */ + public void method1(); + + /* 新类的方法 */ + public void method2(); + } + + public class Source { + public void method1() { + System.out.println("this is original method!"); + } + } + + public class Adapter extends Source implements Targetable { + @Override + public void method2() { + System.out.println("this is the targetable method!"); + } + } + + public class AdapterTest { + public static void main(String[] args) { + Targetable target = new Adapter(); + target.method1(); + target.method2(); + } + } + + 输出结果是: + this is original method! + this is the targetable method! + ``` + + `Adapter`类继承`Source`类,实现`Targetable`接口,这样`Targetable`接口的实现类就具有了`Source`类的功能。 + +- 对象的适配器模式 + + 基本思路和类的适配器模式相同,只是将`Adapter`类作修改,这次不继承`Source`类,而是持有`Source`类的实例,以达到解决兼容性的问题。 + + 只修改上面的`Adapter`类就可以了。 + + ```java + public class Adapter implements Targetable { + private Source source; + public Adapter(Source source){ + super(); + this.source = source; + } + @Override + public void method2() { + System.out.println("this is the targetable method!"); + } + + @Override + public void method1() { + source.method1(); + } + } + + public class AdapterTest { + public static void main(String[] args) { + Source source = new Source(); + Targetable target = new Wrapper(source); + target.method1(); + target.method2(); + } + } + ``` + 输出结果和上面的一样,只是适配器的方法不同。 + + +- 接口的适配器模式 + + 有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的, + 有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法, + 而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。例如`Android ListView`中的`Adapter`就是这样设计的。 + + ```java + public interface ISourceable { + public void method1(); + public void method2(); + public void method3(); + public void method4(); + public void method5(); + public void method6(); + } + + public abstract class BaseAdapter implements ISourceable{ + public void method1() { + + } + public void method2() { + + } + public void method3() { + + } + public void method4() { + + } + public void method5() { + + } + public void method6() { + + } + } + + public class ListAdapter extends BaseAdapter { + // 不用全部实现方法,只需要实现自己需要的就可以了 + public void method4(){ + System.out.println("the sourceable interface's first Sub1!"); + } + } + ``` + + +总结一下三种适配器模式的应用场景: + +- 类的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。 +- 对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个`Adapter`类,持有原类的一个实例,在`Adapter`类的方法中,调用实例的方法就行。 +- 接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类`Adapter`,实现所有方法,我们写别的类的时候,继承抽象类即可。 + + +装饰器模式`(Decorator)` +--- + +装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。 +装饰器模式(Decorator)能够在运行时动态地为原始对象增加一些额外的功能,使其变得更加强大。从某种程度上讲,装饰器非常类似于“继承”,它们都是为了 +增强原始对象的功能,区别在于方式的不同,后者是在编译时(compile-time)静态地通过对原始类的继承完成,而前者则是在程序运行时(run-time)通过对原始 +对象动态地“包装”完成,是对类实例(对象)“装饰”的结果。 + + +`Source`类是被装饰类,`Decorator`类是一个装饰类,可以为`Source`类动态的添加一些功能,代码如下: + +```java +public interface ISourceable { + public void method(); +} + +public class Source implements ISourceable { + @Override + public void method() { + System.out.println("the original method!"); + } +} +public class Decorator implements ISourceable { + private ISourceable source; + public Decorator(ISourceable source){ + super(); + this.source = source; + } + @Override + public void method() { + System.out.println("before decorator!"); + source.method(); + System.out.println("after decorator!"); + } +} +public class DecoratorTest { + public static void main(String[] args) { + ISourceable source = new Source(); + ISourceable obj = new Decorator(source); + obj.method(); + } +} + +输出: +before decorator! +the original method! +after decorator! +``` + +装饰器模式的应用场景: + +- 需要扩展一个类的功能。 +- 动态的为一个对象增加功能,而且还能动态撤销。(继承不能做到这一点,继承的功能是静态的,不能动态增删) + +缺点:产生过多相似的对象,不易排错! + + +代理模式`(Proxy)` +--- + +代理模式(Proxy),顾名思义,有代表打理的意思。某些情况下,当客户端不能或不适合直接访问目标业务对象时,业务对象可以通过代理把自己的业务托管起来, +使客户端间接地通过代理进行业务访问。如此不但能方便用户使用,还能对客户端的访问进行一定的控制。简单来说,就是代理方以业务对象的名义,代理了它的业务。 +代理模式不仅能增强原业务功能,更重要的是还能对其进行业务管控。对用户来讲,隐藏于代理中的实际业务被透明化了,而暴露出来的是代理业务,以此避免客户 +端直接进行业务访问所带来的安全隐患,从而保证系统业务的可控性、安全性。 + +代理模式就是多一个代理类出来,替原对象进行一些操作,比如我们在租房子的时候回去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面, +希望找一个更熟悉的人去帮你做,此处的代理就是这个意思。再如我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作, +表达我们的想法。也就是说把专业事情交给专业的人来做。 + +代理按照代理的创建时期,可以分为两种: + +- 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的`.class`文件就已经存在了。 +- 动态代理:在程序运行时运用反射机制动态创建而成。也就是说我们不需要专门针对某个接口去编写代码实现一个代理类,而是在接口运行时动态生成。 + +静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的 +重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能,主要用的是`Java`的反射机制。 + +下面用静态代理的示例: +```java +public interface ISourceable { + public void method(); +} + +public class Source implements ISourceable { + @Override + public void method() { + System.out.println("the original method!"); + } +} +public class Proxy implements ISourceable { + private Source source; + public Proxy(){ + super(); + this.source = new Source(); + } + @Override + public void method() { + before(); + source.method(); + atfer(); + } + private void atfer() { + System.out.println("after proxy!"); + } + private void before() { + System.out.println("before proxy!"); + } +} + +public class ProxyTest { + public static void main(String[] args) { + Sourceable source = new Proxy(); + source.method(); + } +} + +输出: +before proxy! +the original method! +after proxy! +``` + +代理模式可以有效的将具体的实现与调用方进行解耦,通过面向接口进行编码完全将具体的实现隐藏在内部。 +代理模式的应用场景,如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法: + +- 修改原有的方法来适应。这样违反了“对扩展开放,对修改关闭”的原则。 +- 就是采用一个代理类调用原有的方法,且对产生的结果进行控制。这种方法就是代理模式。 + +使用代理模式,可以将功能划分的更加清晰,有助于后期维护! + +动态代理的优缺点: +- 优点:代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合)。 +- 缺点: + - 代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。 + - 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了 + +即静态代理类只能为特定的接口`(Service)`服务。如想要为多个接口服务则需要建立很多个代理类。 + + +动态代理的思维模式与之前的一般模式是一样的,也是面向接口进行编码,创建代理类将具体类隐藏解耦,不同之处在于代理类的创建时机不同,动态代理需要在运行时因需实时创建。 + +动态代理示例: + +```java +//动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类 +public class DynamicProxy implements InvocationHandler { + private Object object;//用于接收具体实现类的实例对象 + //使用带参数的构造器来传递具体实现类的对象 + public DynamicProxy(Object obj){ + this.object = obj; + } + @Override + public Object invoke(Object proxy, Method method, Object[] args)throws Throwable { + System.out.println("前置内容"); + method.invoke(object, args); + System.out.println("后置内容"); + return null; + } +} + +public static void main(String[] args) { + ISourceable source = new Source(); + InvocationHandler h = new DynamicProxy(source); + ISourceable proxy = (ISourceable) Proxy.newProxyInstance(ISourceable.class.getClassLoader(), new Class[]{ISourceable.class}, h); + proxy.method(); +} +``` + +动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理`(InvocationHandler.invoke)`。这样, +在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强 + +代理对象就是把被代理对象包装一层,在其内部做一些额外的工作,比如用户需要上facebook,而普通网络无法直接访问,网络代理帮助用户先翻墙, +然后再访问facebook。这就是代理的作用了。 + +纵观静态代理与动态代理,它们都能实现相同的功能,而我们看从静态代理到动态代理的这个过程,我们会发现其实动态代理只是对类做了进一步抽象和封装, +使其复用性和易用性得到进一步提升而这不仅仅符合了面向对象的设计理念,其中还有AOP的身影,这也提供给我们对类抽象的一种参考。关于动态代理与AOP的关系, +个人觉得AOP是一种思想,而动态代理是一种AOP思想的实现! + +外观(门面)模式`(Facade)` +--- + + +外观模式是为了解决类与类之家的依赖关系的,像`spring`一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个`Facade` +类中,降低了类类之间的耦合度,该模式中没有涉及到接口。 +它可能是最简单的结构型设计模式,它能将多个不同的子系统接口封装起来,并对外提供统一的高层接口,使复杂的子系统变得更易使用。 +利用门面模式,我们可以把多个子系统“关”在门里面隐藏起来,成为一个整合在一起的大系统,来自外部的访问只需通过这道“门面”(接口)来进行,而不必再 +关心门面背后隐藏的子系统及其如何运转。总之,无论门面内部如何错综复杂,从门面外部看来总是一目了然,使用起来也很简单。 + +为了更形象地理解门面模式,我们先来看一个例子。早期的相机使用起来是非常麻烦的,拍照前总是要根据场景情况进行一系列复杂的操作,如对焦、调节闪光灯、 +调光圈等,非专业人士面对这么一大堆的操作按钮根本无从下手,拍出来的照片质量也不高。随着科技的进步,出现了一种相机,叫作“傻瓜相机”,以形容其使用 +起来的方便性。用户再也不必学习那些复杂的参数调节了,只要按下快门键就可完成所有操作。 + + +下面以计算机启动过程为例: +```java +public class CPU { + public void startup(){ + System.out.println("cpu startup!"); + } + + public void shutdown(){ + System.out.println("cpu shutdown!"); + } +} + +public class Memory { + public void startup(){ + System.out.println("memory startup!"); + } + + public void shutdown(){ + System.out.println("memory shutdown!"); + } +} + +public class Disk { + public void startup(){ + System.out.println("disk startup!"); + } + + public void shutdown(){ + System.out.println("disk shutdown!"); + } +} + +public class Computer { + private CPU cpu; + private Memory memory; + private Disk disk; + + public Computer(){ + cpu = new CPU(); + memory = new Memory(); + disk = new Disk(); + } + + public void startup(){ + System.out.println("start the computer!"); + cpu.startup(); + memory.startup(); + disk.startup(); + System.out.println("start computer finished!"); + } + + public void shutdown(){ + System.out.println("begin to close the computer!"); + cpu.shutdown(); + memory.shutdown(); + disk.shutdown(); + System.out.println("computer closed!"); + } +} + +public class User { + public static void main(String[] args) { + Computer computer = new Computer(); + computer.startup(); + computer.shutdown(); + } +} +start the computer! +cpu startup! +memory startup! +disk startup! +start computer finished! +begin to close the computer! +cpu shutdown! +memory shutdown! +disk shutdown! +computer closed! +``` + +如果我们没有`Computer`类,那么`CPU`、`Memory`、`Disk`他们之间将会相互持有实例,产生关系,这样会造成严重的依赖,修改一个类,可能会带来 +其他类的修改,这不是我们想要看到的,有了`Computer`类,他们之间的关系被放在了`Computer`类里,这样就起到了解耦的作用,这就是外观模式! + + +桥接模式`(Bridge)` +--- + +桥接模式能将抽象与实现分离,使二者可以各自单独变化而不受对方约束,使用时再将它们组合起来,就像架设桥梁一样连接它们的功能,如此降低了抽象与实现 +这两个可变维度的耦合度,以保证系统的可扩展性。 + +桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的`JDBC`桥`DriverManager`一样,`JDBC`进行连接数据库的时候,在各个数据库 +之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是`JDBC`提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。 + +```java +public interface ISourceable { + public void method(); +} + +public class SourceSub1 implements ISourceable { + @Override + public void method() { + System.out.println("this is the first sub!"); + } +} +public class SourceSub2 implements ISourceable { + @Override + public void method() { + System.out.println("this is the second sub!"); + } +} +``` + +定义一个桥,持有`ISourceable`的实例: +```java +public abstract class Bridge { + private ISourceable source; + + public void method(){ + source.method(); + } + + public ISourceable getSource() { + return source; + } + + public void setSource(ISourceable source) { + this.source = source; + } +} +public class MyBridge extends Bridge { + public void method(){ + if (getSource() == null) { + return; + } + getSource().method(); + } +} + +public class BridgeTest { + public static void main(String[] args) { + Bridge bridge = new MyBridge(); + /*调用第一个对象*/ + Sourceable source1 = new SourceSub1(); + bridge.setSource(source1); + bridge.method(); + + /*调用第二个对象*/ + Sourceable source2 = new SourceSub2(); + bridge.setSource(source2); + bridge.method(); + } +} + +输出: + +this is the first sub! +this is the second sub! +``` + +这样,就通过对`Bridge`类的调用,实现了对接口`ISourceable`的实现类`SourceSub1`和`SourceSub2`的调用。 + + +组合模式`(Composite)` +--- + +组合模式是针对由多个节点对象(部分)组成的树形结构的对象(整体)而发展出的一种结构型设计模式,它能够使客户端在操作整体对象或者其下的每个节点对象 +时做出统一的响应,保证树形结构对象使用方法的一致性,使客户端不必关注对象的整体或部分,最终达到对象复杂的层次结构与客户端解耦的目的。 + +组合模式有时又叫部分-整体模式在处理类似树形结构的问题时比较方便。 + +```java +public class TreeNode { + private String name; + private TreeNode parent; + private Vector children = new Vector(); + + public TreeNode(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TreeNode getParent() { + return parent; + } + + public void setParent(TreeNode parent) { + this.parent = parent; + } + + //添加子节点 + public void add(TreeNode node) { + children.add(node); + } + + //删除子节点 + public void remove(TreeNode node) { + children.remove(node); + } + + //获取子节点 + public Enumeration getChildren() { + return children.elements(); + } +} + +public class Tree { + TreeNode root; + + public Tree(String name) { + root = new TreeNode(name); + } +} + +测试代码: +public static void main(String[] args) { + Tree tree = new Tree("A"); + TreeNode nodeB = new TreeNode("B"); + TreeNode nodeC = new TreeNode("C"); + + nodeB.add(nodeC); + tree.root.add(nodeB); + System.out.println("build the tree finished!"); +} +``` +使用场景:将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树,数等。 + +享元模式`(Flyweight)` +--- + +享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flyweight_1.jpg?raw=true) + +`FlyWeightFactory`负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象, +如果没有,则创建一个新对象,`FlyWeight`是超类。一提到共享池,我们很容易联想到`Java`里面的`JDBC`连接池,想想每个连接的特点, +我们不难总结出:适用于作共享的一些个对象,他们有一些共有的属性,就拿数据库连接池来说,`url`、`driverClassName`、`username`、`password` +及`dbname`,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据, +在方法调用时,当做参数传进来,这样就节省了空间,减少了实例的数量。 + +下面用数据库连接池的例子: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flyweight_2.jpg?raw=true) + +```java +public class ConnectionPool { + private Vector pool; + /*公有属性*/ + private String url = "jdbc:mysql://localhost:3306/test"; + private String username = "root"; + private String password = "root"; + private String driverClassName = "com.mysql.jdbc.Driver"; + + private int poolSize = 100; + Connection conn; + + /*构造方法,做一些初始化工作*/ + private ConnectionPool() { + pool = new Vector<>(poolSize); + for (int i = 0; i < poolSize; i++) { + try { + Class.forName(driverClassName); + conn = DriverManager.getConnection(url, username, password); + pool.add(conn); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + /* 返回连接到连接池 */ + public synchronized void release() { + pool.add(conn); + } + + /* 返回连接池中的一个数据库连接 */ + public synchronized Connection getConnection() { + if (pool.size() > 0) { + Connection conn = pool.get(0); + pool.remove(conn); + return conn; + } else { + return null; + } + } +} +``` + +通过连接池的管理,实现了数据库连接的共享,不需要每一次都重新创建连接,节省了数据库重新创建的开销,提升了系统的性能! + + +策略模式`(strategy)` +--- + +策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供 +统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数,关系图如下: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/strategy.jpg?raw=true) + +图中`ICalculator`提供了同样的方法, +`AbstractCalculator`是辅助类,提供辅助方法,接下来,依次实现下每个类: + +```java +public interface ICalculator { + public int calculate(String exp); +} +public abstract class AbstractCalculator { + + public int[] split(String exp,String opt){ + String array[] = exp.split(opt); + int arrayInt[] = new int[2]; + arrayInt[0] = Integer.parseInt(array[0]); + arrayInt[1] = Integer.parseInt(array[1]); + return arrayInt; + } +} + +具体的实现类: +public class Plus extends AbstractCalculator implements ICalculator { + + @Override + public int calculate(String exp) { + int arrayInt[] = split(exp,"\\+"); + return arrayInt[0]+arrayInt[1]; + } +} +public class Minus extends AbstractCalculator implements ICalculator { + + @Override + public int calculate(String exp) { + int arrayInt[] = split(exp,"-"); + return arrayInt[0]-arrayInt[1]; + } +} +public class Multiply extends AbstractCalculator implements ICalculator { + + @Override + public int calculate(String exp) { + int arrayInt[] = split(exp,"\\*"); + return arrayInt[0]*arrayInt[1]; + } +} + +测试类: +public class StrategyTest { + + public static void main(String[] args) { + String exp = "2+8"; + ICalculator cal = new Plus(); + int result = cal.calculate(exp); + System.out.println(result); + } +} +输出: +10 +``` +策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用 +哪个算法即可 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/design_mode_celue.png?raw=true) + +相信大家对上图中的计算机、USB接口还有各种设备之间的关系以及使用方法都非常熟悉了,这些模块组成的系统正是策略模式的最佳范例。策略接口就是图中的 +USB接口。 + +我们通过对计算机USB接口的标准化,使计算机系统拥有了无限扩展外设的能力,需要什么功能只需要购买相关的USB设备。可见在策略模式中,USB接口起到了 +至关重要的解耦作用。如果没有USB接口的存在,我们就不得不将外设直接“焊接”在主机上,致使设备与主机高度耦合,系统将彻底丧失对外设的替换与扩展能力。 + +变化是世界的常态,唯一不变的就是变化本身。拥有顺势而为、随机应变的能力才能立于不败之地。策略模式的运用能让系统的应变能力得到提升,适应随时变化 +的需求。接口的巧妙运用让一系列的策略可以脱离系统而单独存在,使系统拥有更灵活、更强大的“可插拔”扩展功能。 + +模板方法模式`(Template Method)` +--- + +模板是对多种事物的结构、形式、行为的模式化总结,而模板方法模式则是对一系列类行为(方法)的模式化。我们将总结出来的行为规律固化在基类中, +对具体的行为实现则进行抽象化并交给子类去完成,如此便实现了子类对基类模板的套用。 + +一个抽象类中,有一个主方法,再定义1...n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类, +实现对子类的调用,先看个关系图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/template.jpg?raw=true) + +就是在`AbstractCalculator`类中定义一个主方法`calculate()`,`calculate()`调用`spilt()`等,`Plus`和`Minus`分别继承 +`AbstractCalculator`类,通过对`AbstractCalculator`的调用实现对子类的调用,看下面的例子: + +```java +public abstract class AbstractCalculator { + /*主方法,实现对本类其它方法的调用*/ + public final int calculate(String exp,String opt){ + int array[] = split(exp,opt); + return calculate(array[0],array[1]); + } + + /*被子类重写的方法*/ + abstract public int calculate(int num1,int num2); + + public int[] split(String exp,String opt){ + String array[] = exp.split(opt); + int arrayInt[] = new int[2]; + arrayInt[0] = Integer.parseInt(array[0]); + arrayInt[1] = Integer.parseInt(array[1]); + return arrayInt; + } +} + +public class Plus extends AbstractCalculator { + + @Override + public int calculate(int num1,int num2) { + return num1 + num2; + } +} +``` + +测试类: + +```java +public static void main(String[] args) { + String exp = "8+8"; + AbstractCalculator cal = new Plus(); + int result = cal.calculate(exp, "\\+"); + System.out.println(result); +} +``` + + +观察者模式`(Observer)` +--- + +观察者模式(Observer)可以针对被观察对象与观察者对象之间一对多的依赖关系建立起一种行为自动触发机制,当被观察对象状态发生变化时主动对外发起广播, +以通知所有观察者做出响应。 + +观察者模式很好理解,类似于邮件订阅和`RSS`订阅,当我们浏览一些博客或`wiki`时,经常会看到`RSS`图标,就这的意思是,当你订阅了该文章,如果后续有 +更新,会及时通知你。简单的说就是当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。 + +在`Android`中我们常用的`Button.setOnClickListener()`其实就是用的观察者模式。大名鼎鼎的`RxJava`也是基于这种模式。 + +观察者: +```java +public interface Observer { + public void update(); +} + +public class Observer1 implements Observer { + @Override + public void update() { + System.out.println("observer1 has received!"); + } +} +public class Observer2 implements Observer { + @Override + public void update() { + System.out.println("observer2 has received!"); + } + +} +``` +被观察者: +```java +public interface Subject { + /*增加观察者*/ + public void add(Observer observer); + + /*删除观察者*/ + public void del(Observer observer); + + /*通知所有的观察者*/ + public void notifyObservers(); + + /*自身的操作*/ + public void operation(); +} + +public abstract class AbstractSubject implements Subject { + private Vector vector = new Vector<>(); + @Override + public void add(Observer observer) { + vector.add(observer); + } + + @Override + public void del(Observer observer) { + vector.remove(observer); + } + + @Override + public void notifyObservers() { + Enumeration enumo = vector.elements(); + while(enumo.hasMoreElements()){ + enumo.nextElement().update(); + } + } +} + +public class MySubject extends AbstractSubject { + @Override + public void operation() { + System.out.println("update self!"); + // 一旦发生了观察者所需要的动作,就需要去通知观察者们 + notifyObservers(); + } + +} +``` + +测试类: + +```java +public static void main(String[] args) { + Subject sub = new MySubject(); + sub.add(new Observer1()); + sub.add(new Observer2()); + + sub.operation(); +} +``` + +输出: + +```java +update self! +observer1 has received! +observer2 has received! +``` + + +迭代器模式`(Iterator)` +--- + +迭代,在程序中特指对某集合中各元素逐个取用的行为。迭代器模式提供了一种机制来按顺序访问集合中的各元素,而不需要知道集合内部的构造。 +换句话讲,迭代器满足了对集合迭代的需求,并向外部提供了一种统一的迭代方式,而不必暴露集合的内部数据结构。 + +迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。 +这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。 + +`MyCollection`中定义了集合的一些操作,`MyIterator`中定义了一系列迭代操作,且持有`Collection`实例,我们来看看实现代码: + +```java +public interface Collection { + public Iterator iterator(); + + /*取得集合元素*/ + public Object get(int i); + + /*取得集合大小*/ + public int size(); +} + +public interface Iterator { + //前移 + public Object previous(); + + //后移 + public Object next(); + public boolean hasNext(); + + //取得第一个元素 + public Object first(); +} +``` + +具体实现类: +```java +public class MyCollection implements Collection { + public String string[] = {"A","B","C","D","E"}; + @Override + public Iterator iterator() { + return new MyIterator(this); + } + + @Override + public Object get(int i) { + return string[i]; + } + + @Override + public int size() { + return string.length; + } +} +public class MyIterator implements Iterator { + private Collection collection; + private int pos = -1; + + public MyIterator(Collection collection){ + this.collection = collection; + } + + @Override + public Object previous() { + if(pos > 0){ + pos--; + } + return collection.get(pos); + } + + @Override + public Object next() { + if(pos < collection.size()-1){ + pos++; + } + return collection.get(pos); + } + + @Override + public boolean hasNext() { + if(pos < collection.size()-1){ + return true; + }else{ + return false; + } + } + + @Override + public Object first() { + pos = 0; + return collection.get(pos); + } +} +``` + +测试类: +```java +public static void main(String[] args) { + Collection collection = new MyCollection(); + Iterator it = collection.iterator(); + + while(it.hasNext()){ + System.out.println(it.next()); + } +} +``` + +输出: +``` +A B C D E +``` + +foreach是Java5中引入的一种for的语法增强。如果我们对class文件进行反编译就会发现,对Collection接口的各种实现类来说, +foreach本质上还是通过获取迭代器(Iterator)来遍历的。 + +对于任何类型的集合,要防止内部机制不被暴露或破坏,以及确保用户对每个元素有足够的访问权限,迭代器模式起到了至关重要的作用。 +迭代器巧妙地利用了内部类的形式与集合类分离,迭代器依然对其内部的元素保有访问权限,如此便促成了集合的完美封装,在此基础上还提供给用户一套标准的 +迭代器接口,使各种繁杂的遍历方式得以统一。迭代器模式的应用,能在内部事务不受干涉的前提下,保持一定的对外部开放,让我们“鱼与熊掌兼得”。 + +责任链模式`(Chain of Responsibility)` +--- + +责任链是由很多责任节点串联起来的一条任务链条,其中每一个责任节点都是一个业务处理环节。责任链模式允许业务请求者将责任链视为一个整体并对其发起请求, +而不必关心链条内部具体的业务逻辑与流程走向,也就是说,请求者不必关心具体是哪个节点起了作用,总之业务最终能得到相应的处理。在软件系统中,当一个 +业务需要经历一系列业务对象去处理时,我们可以把这些业务对象串联起来成为一条业务责任链,请求者可以直接通过访问业务责任链来完成业务的处理,最终实现 +请求者与响应者的解耦。 + +有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终那个对 +象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整。先看看关系图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/zerenlian.jpg?raw=true) + +`Abstracthandler`类提供了`get`和`set`方法,方便`MyHandler`类设置和修改引用对象,`MyHandler`类是核心,实例化后生成一系列相互持有的对象,构成一条链。 + +```java +public interface IHandler { + public void operator(); +} + +public abstract class AbstractHandler { + private IHandler handler; + + public IHandler getHandler() { + return handler; + } + + public void setHandler(IHandler handler) { + this.handler = handler; + } +} + +public class MyHandler extends AbstractHandler implements IHandler { + private String name; + + public MyHandler(String name) { + this.name = name; + } + + @Override + public void operator() { + System.out.println(name+"deal!"); + if(getHandler()!=null){ + getHandler().operator(); + } + } +} +``` + +测试类: +```java + public static void main(String[] args) { + MyHandler h1 = new MyHandler("h1"); + MyHandler h2 = new MyHandler("h2"); + MyHandler h3 = new MyHandler("h3"); + + h1.setHandler(h2); + h2.setHandler(h3); + + h1.operator(); +} +``` + +输出: +``` +h1deal! +h2deal! +h3deal! +``` +链接上的请求可以是一条链,可以是一个树,还可以是一个环,模式本身不约束这个,需要我们自己去实现,同时,在一个时刻,命令只允许由一个对象传给另一个 +对象,而不允许传给多个对象。 + +命令模式`(Command)` +--- + +命令模式很好理解,举个例子,司令员下令让士兵去干件事情,从整个事情的角度来考虑,司令员的作用是,发出口令,口令经过传递,传到了士兵耳朵里, +士兵去执行。这个过程好在,三者相互解耦,任何一方都不用去依赖其他人,只需要做好自己的事儿就行,司令员要的是结果,不会去关注到底士兵是怎么实现的。 +我们看看关系图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/command.jpg?raw=true) + +`Invoker`是调用者(司令员),`Receiver`是被调用者(士兵),`MyCommand`是命令,实现了`Command`接口,持有接收对象,实现代码: + +```java +public interface Command { + public void exe(); +} + +public class MyCommand implements Command { + private Receiver receiver; + + public MyCommand(Receiver receiver) { + this.receiver = receiver; + } + + @Override + public void exe() { + receiver.action(); + } +} +public class Receiver { + public void action(){ + System.out.println("command received!"); + } +} + +public class Invoker { + private Command command; + + public Invoker(Command command) { + this.command = command; + } + + public void action(){ + command.exe(); + } +} + +public static void main(String[] args) { + Receiver receiver = new Receiver(); + Command cmd = new MyCommand(receiver); + Invoker invoker = new Invoker(cmd); + invoker.action(); + // 输出:command received! +} +``` + +命令模式的目的就是达到命令的发出者和执行者之间解耦,实现请求和执行分开,熟悉`Struts`的同学应该知道,`Struts`其实就是一种将请求和呈现分离的 +技术,其中必然涉及命令模式的思想! + + +备忘录模式`(Memento)` +--- + +备忘录用来记录曾经发生过的事情,使回溯历史变得切实可行。备忘录模式则可以在不破坏元对象封装性的前提下捕获其在某些时刻的内部状态,并像历史快照一样 +将它们保留在元对象之外,以备恢复之用。 + +主要目的是保存一个对象的某个状态,以便在适当的时候恢复对象,个人觉得叫备份模式更形象些,通俗的讲下:假设有原始类A,A中有各种属性,A可以决定需要 +备份的属性,备忘录类B是用来存储A的一些内部状态,类C呢,就是一个用来存储备忘录的,且只能存储,不能修改等操作。做个图来分析一下: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/memento.jpg?raw=true) + +`Original`类是原始类,里面有需要保存的属性`value`及创建一个备忘录类,用来保存`value`值。`Memento`类是备忘录类,`Storage`类是存储备忘录 +的类,持有`Memento`类的实例,该模式很好理解。直接看源码: + +```java +public class Original { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Original(String value) { + this.value = value; + } + + public Memento createMemento(){ + return new Memento(value); + } + + public void restoreMemento(Memento memento){ + this.value = memento.getValue(); + } +} + +public class Memento { + private String value; + + public Memento(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} + +public class Storage { + private Memento memento; + + public Storage(Memento memento) { + this.memento = memento; + } + + public Memento getMemento() { + return memento; + } + + public void setMemento(Memento memento) { + this.memento = memento; + } +} + +测试类: +public static void main(String[] args) { + // 创建原始类 + Original origi = new Original("egg"); + + // 创建备忘录 + Storage storage = new Storage(origi.createMemento()); + + // 修改原始类的状态 + System.out.println("初始化状态为:" + origi.getValue()); + origi.setValue("niu"); + System.out.println("修改后的状态为:" + origi.getValue()); + + // 回复原始类的状态 + origi.restoreMemento(storage.getMemento()); + System.out.println("恢复后的状态为:" + origi.getValue()); +} + +输出: + +初始化状态为:egg +修改后的状态为:niu +恢复后的状态为:egg +``` +新建原始类时,`value`被初始化为`egg`,后经过修改,将`value`的值置为`niu`,最后倒数第二行进行恢复状态,结果成功恢复了。 +其实我觉得这个模式叫“备份-恢复”模式最形象。 + + +状态模式`(State)` +--- + +状态指事物基于所处的状况、形态表现出的不同的行为特性。状态模式构架出一套完备的事物内部状态转换机制,并将内部状态包裹起来且对外部不可见, +使其行为能随其状态的改变而改变,同时简化了事物的复杂的状态变化逻辑。 + +核心思想就是:当对象的状态改变时,同时改变其行为,很好理解!就拿QQ来说,有几种状态,在线、隐身、忙碌等,每个状态对应不同的操作, +而且你的好友也能看到你的状态,所以,状态模式就两点: + +- 可以通过改变状态来获得不同的行为 +- 你的好友能同时看到你的变化 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/state.jpg?raw=true) + +`State`类是个状态类,`Context`类可以实现切换,我们来看看代码: + +```java +public class State { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public void method1(){ + System.out.println("execute the first opt!"); + } + + public void method2(){ + System.out.println("execute the second opt!"); + } +} + +public class Context { + private State state; + + public Context(State state) { + this.state = state; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public void method() { + if (state.getValue().equals("state1")) { + state.method1(); + } else if (state.getValue().equals("state2")) { + state.method2(); + } + } +} +``` + +测试类: + +```java +public static void main(String[] args) { + State state = new State(); + Context context = new Context(state); + + //设置第一种状态 + state.setValue("state1"); + context.method(); + + //设置第二种状态 + state.setValue("state2"); + context.method(); +} + +输出: +execute the first opt! +execute the second opt! +``` + +根据这个特性,状态模式在日常开发中用的挺多的,尤其是做网站的时候,我们有时希望根据对象的某一属性,区别开他们的一些功能,比如说简单的权限控制等。 + + + +访问者模式`(Visitor)` +--- +访问者模式主要解决的是数据与算法的耦合问题,尤其是在数据结构比较稳定,而算法多变的情况下。为了不“污染”数据本身,访问者模式会将多种算法独立归类, +并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并且确保算法的自由扩展。 + +访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式 +使的算法操作增加变的容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易, +因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。 + +简单来说,访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。 + +示例:银行柜台提供的服务和来办业务的人。把银行的服务和业务的办理解耦了。 +缺点:如果银行要修改底层业务接口,所有继承接口的类都需要作出修改。不过java8的新特性接口默认方法可以解决这个问题,或者java8之前可以通过接口的 +适配器模式来解决这个问题。 + +```java + // 银行柜台服务,以后银行要新增业务,只需要新增一个类实现这个接口就可以了。 + public interface Service { + void accept(Visitor visitor); + } + + // 来办业务的人,里面可以加上权限控制等等 + static class Visitor { + public void process(Service service) { + // 基本业务 + System.out.println("基本业务"); + } + + public void process(Saving service) { + // 存款 + System.out.println("存款"); + } + + public void process(Draw service) { + // 提款 + System.out.println("提款"); + } + + public void process(Fund service) { + System.out.println("基金"); + // 基金 + } + } + + static class Saving implements Service { + public void accept(Visitor visitor) { + visitor.process(this); + } + } + + static class Draw implements Service { + public void accept(Visitor visitor) { + visitor.process(this); + } + } + + static class Fund implements Service { + public void accept(Visitor visitor) { + visitor.process(this); + } + } + + public static void main(String[] args) { + Service saving = new Saving(); + Service fund = new Fund(); + Service draw = new Draw(); + Visitor visitor = new Visitor(); + Visitor guweiwei = new Visitor(); + fund.accept(guweiwei); + saving.accept(visitor); + fund.accept(visitor); + draw.accept(visitor); + // 输出: + // 基金 + // 存款 + // 基金 + // 提款 + } +} +``` + +该模式适用场景:如果我们想为一个现有的类增加新功能,不得不考虑几个事情: + +- 新功能会不会与现有功能出现兼容性问题? +- 以后会不会再需要添加? +- 如果类不允许修改代码怎么办? + +面对这些问题,最好的解决方法就是使用访问者模式,访问者模式适用于数据结构相对稳定的系统,把数据结构和算法解耦, + +中介者模式`(Mediator)` +--- + +中介是在事物之间传播信息的中间媒介。中介模式为对象构架出一个互动平台,通过减少对象间的依赖程度以达到解耦的目的。我们的生活中有各种各样的媒介, +如婚介所、房产中介、门户网站、电子商务、交换机组网、通信基站、即时通软件等,这些都与人类的生活息息相关,离开它们我们将举步维艰。 + +中介者模式也是用来降低类类之间的耦合的,因为如果类类之间有依赖关系的话,不利于功能的拓展和维护,因为只要修改一个对象,其它关联的对象都得进行修改。 +如果使用中介者模式,只需关心和`Mediator`类的关系,具体类类之间的关系及调度交给`Mediator`就行,这有点像`spring`容器的作用。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mediator.jpg?raw=true) + +`User`类统一接口,`User1`和`User2`分别是不同的对象,二者之间有关联,如果不采用中介者模式,则需要二者相互持有引用,这样二者的耦合度很高,为了解耦, +引入了`Mediator`类,提供统一接口,`MyMediator`为其实现类,里面持有`User1`和`User2`的实例,用来实现对`User1`和`User2`的控制。 + +这样`User1`和`User2`两个对象相互独立,他们只需要保持好和`Mediator`之间的关系就行,剩下的全由`MyMediator`类来维护。 + +```java +public interface IMediator { + public void createMediator(); + public void workAll(); +} + + +public class MyMediator implements IMediator { + private User user1; + private User user2; + + public User getUser1() { + return user1; + } + + public User getUser2() { + return user2; + } + + @Override + public void createMediator() { + user1 = new User1(this); + user2 = new User2(this); + } + + @Override + public void workAll() { + user1.work(); + user2.work(); + } +} + + +public abstract class User { + private Mediator mediator; + + public Mediator getMediator(){ + return mediator; + } + + public User(Mediator mediator) { + this.mediator = mediator; + } + + public abstract void work(); +} + +public class User1 extends User { + public User1(Mediator mediator){ + super(mediator); + } + + @Override + public void work() { + System.out.println("user1 exe!"); + } +} + +public class User2 extends User { + public User2(Mediator mediator){ + super(mediator); + } + + @Override + public void work() { + System.out.println("user2 exe!"); + } +} +``` + +测试类: +```java +public static void main(String[] args) { + Mediator mediator = new MyMediator(); + mediator.createMediator(); + mediator.workAll(); + // 输出: + // user1 exe! + // user2 exe! +} +``` +众所周知,对象间显式的互相引用越多,意味着依赖性越强,同时独立性越弱,不利于代码的维护与扩展。中介模式很好地解决了这些问题,它能将多方互动的工作 +交由中间平台去完成,解除了你中有我、我中有你的相互依赖,让各个模块之间的关系变得更加松散、独立,最终增强系统的可复用性与可扩展性,同时也使系统 +运行效率得到提升。 + +解释器模式`(Interpreter)` +--- + +解释有拆解、释义的意思,一般可以理解为针对某段文字,按照其语言的特定语法进行解析,再以另一种表达形式表达出来,以达到人们能够理解的目的。类似地, +解释器模式(Interpreter)会针对某种语言并基于其语法特征创建一系列的表达式类(包括终极表达式与非终极表达式),利用树结构模式将表达式对象组装起来, +最终将其翻译成计算机能够识别并执行的语义树。例如结构型数据库对查询语言SQL的解析,浏览器对HTML语言的解析,以及操作系统Shell对命令的解析。 +不同的语言有着不同的语法和翻译方式,这都依靠解释器完成。以最常见的Java编程语言为例。当我们以人类能够理解的语言完成了一段程序并命名为 +Hello.java后,经过调用编译器会生成Hello.class的字节码文件,执行的时候则会加载此文件到内存并进行解释、执行,最终被解释的机器码才是计算机可以 +理解并执行的指令格式,如下图所示。从Java语言到机器语言,这个跨越语言鸿沟的翻译步骤必须由解释器来完成,这便是其存在的意义: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/design_mode_jieshiqi.png?raw=true) + +解释器模式是我们暂时的最后一讲,一般主要应用在`OOP`开发中的编译器的开发中,所以适用面比较窄。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interpreter.jpg?raw=true) + +`Context`类是一个上下文环境类,`Plus`和`Minus`分别是用来计算的实现,代码如下: + +```java +public interface Expression { + public int interpret(Context context); +} + +public class Plus implements Expression { + @Override + public int interpret(Context context) { + return context.getNum1()+context.getNum2(); + } +} + +public class Minus implements Expression { + @Override + public int interpret(Context context) { + return context.getNum1()-context.getNum2(); + } +} + +public class Context { + private int num1; + private int num2; + + public Context(int num1, int num2) { + this.num1 = num1; + this.num2 = num2; + } + + public int getNum1() { + return num1; + } + public void setNum1(int num1) { + this.num1 = num1; + } + public int getNum2() { + return num2; + } + public void setNum2(int num2) { + this.num2 = num2; + } +} +``` + +测试类: +```java +public static void main(String[] args) { + // 计算9+2-8的值 + int result = new Minus().interpret((new Context(new Plus() + .interpret(new Context(9, 2)), 8))); + System.out.println(result); + // 输出:3 +} +``` +基本就这样,解释器模式用来做各种各样的解释器,如正则表达式等的解释器等等! + +在面向对象的软件设计中,人们经常会遇到一些重复出现的问题。为降低软件模块的耦合性,提高软件的灵活性、兼容性、可复用性、可维护性与可扩展性, +人们从宏观到微观对各种软件系统进行拆分、抽象、组装,确立模块间的交互关系,最终通过归纳、总结,将一些软件模式沉淀下来成为通用的解决方案, +这就是设计模式的由来与发展。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Java\345\237\272\347\241\200/Git\345\221\275\344\273\244.md" "b/Java\345\237\272\347\241\200/Git\345\221\275\344\273\244.md" deleted file mode 100644 index fb5d1241..00000000 --- "a/Java\345\237\272\347\241\200/Git\345\221\275\344\273\244.md" +++ /dev/null @@ -1,175 +0,0 @@ -Git命令 -=== - -先上一张图 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.jpg) -图中的index部分就是暂存区 - -- 安装好git后我们要先配置一下。以便`git`跟踪。 - ``` - git config --global "user.name xxx" - git config --global "user.email xxx@xxx.com" - ``` - 上面修改后可以使用`cat ~/.gitconfig`查看 - 如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 - `git config --list`来查看所有的配置。 - -- 新建仓库 - `mkdir gitDemo` - `cd gitDemo` - `git init` - 这样就创建完了。 - -- clone仓库 - 在某一目录下执行. - `git clone [git path]` - 只是后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 - -- `git add`提交文件更改(修改和新增),把当前的修改添加到暂存区 - `git add xxx.txt`添加某一个文件 - `git add .`添加当前目录所有的文件 - -- `git commit`提交,把修改由暂存区提交到仓库中 - `git commit`提交,然后在出来的提示框内查看当前提交的内容以及输入注释。 - 或者也可以用`git commit -m "xxx"` 提交到本地仓库并且注释是xxx - -- `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 - -- `git diff`对比区别 - `git diff` 直接查看所有的区别 - `git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 - `git diff`比较只是当前工作区和暂存区之间的区别。如果想要查看暂存起来的文件和上次提交时的差异可以使用`git diff --staged`。或者想看和远程仓库最新内容的差异可以使用`git diff HEAD`。 - -- `git push` 提交到远程仓库 - 可以直接调用`git push`推送到当前分支 - 或者`git push origin master`推送到远程`master`分支 - `git push origin devBranch`推送到远程`devBranch`分支 - -- `git merge`合并目标分支到当前分支 - `git merge devBrach` - -- `git log`查看当前分支下的提交记录 - -- `git reset`命令回退到某一版本(回退已经暂存的文件) - `Git`中用`HEAD`表示当前版本,上一版本就是`HEAD^`,上上一版本就是`HEAD^^`.如果往前一千个版本呢? 那就是`HEAD~1000`. - `git reset —-hard HEAD^` - `git reset —-hard commit_id` - `git reset HEAD fileName`可以把用`git add`之后但是还没有`commit`之前暂存区中的修改撤销。 - 说到这里就说一个问题,如果你reset到某一个版本之后,发现弄错了,还想返回去,这时候用`git log`已经找不到之前的`commit id`了。那怎么办?这时候可以使用下面的命令来找。 - -- `git reflog` - 可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 - -- `git checkout`撤销修改或者切换分支 - `git checkout -- xx.txt`意思就是将`xx.txt`文件在工作区的修改全部撤销。可能会有两种情况: - - - 修改后还没有调用`git add`添加到暂存区,现在撤销后就会和版本库一样的状态。 - - 修改后已经调用`git add`添加到暂存区后又做了修改,这时候撤销就会回到暂存区的状态。 - - 总的来说`git checkout`就是让这个文件回到最近一次`git commit`或者`git add`的状态。 - 这里还有一个问题就是我胡乱修改了某个文件内容然后调用了`git add`添加到缓存区中,这时候想丢弃修改该怎么办?也是要分两步: - - 使用`git reset HEAD file`命令,将暂存区中的内容回退,这样修改的内容会从暂存区回到工作区。 - - 使用`git checkout --file`直接丢弃工作区的修改。 - - `git checkout`把当前目录所有修改的文件从`HEAD`都撤销修改。 - 为什么分支的地方也是用`git checkout`这里撤销还是用它呢?他们的区别在于`--`,如果没有`--`那就是检出分支了。 - `git checkout origin/developer` // 切换到orgin/developer分支 - -- `git rm`删除文件 - 该文件就不再纳入版本管理了。如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母),以防误删除文件后丢失修改的内容。 - 另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。比如一些大型日志文件或者一堆 .a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上,用 --cached 选项即可:`git rm --cached readme.txt` - -- `git push` - 把本地仓库的内容推送到远程仓库中。 - -- 分支 - `git`分支的创建和合并都是非常快的,因为增加一个分支其实就是增加一个指针,合并其实就是让某个分支的指针指向某一个位置。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_master_branch.png?raw=true) - - 创建分支 - `git branch devBranch`创建名为`devBranch`的分支。 - `git checkout devBranch`切换到`devBranch`分支。 - `git branch`查看当前仓库中的分支 - `git branch -r`查看远程仓库的分支 - ``` - origin/HEAD -> origin/master - origin/developer - origin/developer_sg - origin/master - origin/master_sg - origin/offline - ``` - `git branch -d devBranch`删除`devBranch`分支。 - 当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有被合并,删除失败。 - 这时如果你要强行删除的话可以使用命令`git branch -D xxx`. - 如何删除远程分支呢? - ``` - git branch -r -d origin/developer - git push origin :developer - ``` - 如何本地创建分支并推送给远程仓库? - ``` - // 本地创建分支 - git checkout master //进入master分支 - git checkout -b frommaster //以master为源创建分支frommaster - // 推送到远程仓库 - git push origin frommaster// 推送到远程仓库所要使用的名字 - ``` - - 如何切到到远程仓库分支进行开发呢? - git checkout -b frommaster origin/frommaster// 本地新建frommaster分支并且与远程仓库的frommaster分支想关联 - 提交更改的话就用 - git push origin frommaster - - -- `git merge`合并指定分支到当前分支 - `git merge devBranch`将`devBranch`分支合并到`master`。 - -- 打`tag` - `git tag v1.0`来进行打`tag`,默认为`HEAD` - `git tag`查看所有`tag` - 如果我想在之前提交的某次`commit`上打`tag`,`git tag v1.0 commitID` - 当然也可以在打`tag`时带上参数 `git tag v1.0 -m "version 1.0 released" commitID` - - `git show tagName`来查看某`tag`的详细信息。 -- 打完`tag`后怎么推送到远程仓库 - `git push origin tagName` - -- 删除`tag` - `git tag -d tagName` - -- 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 - `git push origin:refs/tags/tagName` - -- 忽略文件 - 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 - 其实不用一个个的去写,具体可以根据项目参考[https://github.com/github/gitignore](https://github.com/github/gitignore) - 当然不要忘了把该文件提交上去 - 在用`linux`的时候会自动生成一些以`~`结尾的备份文件,如果ignore掉呢?[https://github.com/github/gitignore/blob/master/Global/Linux.gitignore](https://github.com/github/gitignore/blob/master/Global/Linux.gitignore) - -- 撤销最后一次提交 - 有时候我们提交完了才发现漏掉了几个文件没有加或者提交信息写错了,想要撤销刚才的的提交操作。可以使用--amend选项重新提交:`git commit --amend` - -- 查看远程仓库克隆地址 - `git remote -v` - -关于`git`的工作区、缓存区可以看下图`index`标记部分的区域就是暂存区 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_stage.jpg?raw=true) - -从这个图中能看到缓存区的存在,这就是为什么我们新加或者修改之后都要调用`git add`方法后再调用`git commit`。 -其实我一直有点分不开`reset`和`checkout`的区别,从这个图里能明显看出来了: - -- 当执行`git reset HEAD`命令时,暂存区的目录树会被重写,会被`master`分支指向的目录树所替换,但是工作区不受影响。 -- 当执行`git checkout .`或`git checkout -- file`命令是,会用暂存区全部的文件或指定的文件替换工作区的文件。这个操作很危险,会清楚工作区中未添加到暂存区的改动。 -命令时,会用`HEAD`指向的`master`分支中的全部或部分文件替换暂存区和工作区中的文件。这个命令也是极度危险的。因为不但会清楚工作区中未提交的改动,也会清楚暂存区中未提交的改动。 -- `git reset HEAD ` 是在添加到暂存区后,撤出暂存区使用,他只会把文件撤出暂存区,但是你的修改还在,仍然在工作区。当然如果使用`git reset --hard HEAD`这样就完了,工作区所有的内容都会被远程仓库最新代码覆盖。 -- `git checkout -- xxx.txt`是用于修改后未添加到暂存区时使用(如果修改后添加到暂存区后就没效果了,必须要先reset撤销暂存区后再使用checkout),这时候会把之前的修改覆盖掉。所以是危险的。 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/Java\345\237\272\347\241\200/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" deleted file mode 100644 index d1808e42..00000000 --- "a/Java\345\237\272\347\241\200/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" +++ /dev/null @@ -1,127 +0,0 @@ -HashMap实现原理分析 -=== - -`HashMap`主要是用数组来存储数据的,我们都知道它会对`key`进行哈希运算,哈系运算会有重复的哈希值,对于哈希值的冲突,`HashMap`采用链表来解决的。 -`在HashMap`里有这样的一句属性声明: -```java -transient Entry[] table; -``` -可以看到`Map`是通过数组的方式来储存`Entry`那`Entry`是神马呢?就是`HashMap`存储数据所用的类,它拥有的属性如下: -```java -static class Entry implements Map.Entry { - final K key; - V value; - Entry next; - final int hash; - ...//More code goes here -} -``` -看到`next`了吗?`next`就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry,那么好吧, -将新加的元素也放到第10个位置,将第10个位置的原有`Entry`赋值给当前新加的`Entry`的`next`属性。数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。 - -好了,总结一下: - -- `HashMap`中有一个叫`table`的`Entry`数组。 -- 这个数组存储了`Entry`类的对象。`HashMap`类有一个叫做`Entry`的内部类。这个`Entry`类包含了`key-value`作为实例变量。 -- 每当往`Hashmap`里面存放`key-value`对的时候,都会为它们实例化一个`Entry`对象,这个`Entry`对象就会存储在前面提到的`Entry`数组`table`中。 -现在你一定很想知道,上面创建的`Entry`对象将会存放在具体哪个位置(在`table`中的精确位置)。答案就是,根据`key`的`hashcode()`方法计算出来的`hash`值来决定。 -`hash`值用来计算`key`在`Entry`数组的索引。 -- 我们往`hashmap`放了4个`key-value`对,但是有时候看上去好像只有2个元素!!!这是因为,如果两个元素有相同的`hashcode`,它们会被放在同一个索引上。 -问题出现了,该怎么放呢?原来它是以链表`(LinkedList)`的形式来存储的。 - -接下来看一下`put`方法: -```java -/** -* Associates the specified value with the specified key in this map. If the -* map previously contained a mapping for the key, the old value is -* replaced. -* -* @param key -* key with which the specified value is to be associated -* @param value -* value to be associated with the specified key -* @return the previous value associated with key, or null -* if there was no mapping for key. (A null return -* can also indicate that the map previously associated -* null with key.) -*/ -public V put(K key, V value) { - // 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。 - if (key == null) - return putForNullKey(value); - // 计算key的hash值,hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数 - int hash = hash(key.hashCode()); - // indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引 - int i = indexFor(hash, table.length); - - for (Entry e = table[i]; e != null; e = e.next) { - // 如果这个位置已经有了(也就是hash值一样)就用链表来存了。 开始迭代链表 - Object k; - // 直到Entry->next为null,就把当前的Entry对象变成链表的下一个节点。 - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { - // 如果我们再次放入同样的key会怎样呢?它会替换老的value。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)), - // 如果这个方法返回true,它就会用当前Entry的value来替换之前的value。 - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - // 如果计算出来的索引位置没有元素,就直接把Entry对象放到那个索引上。 - modCount++; - addEntry(hash, key, value, i); - return null; -} -``` -再看一下`get`方法:     -```java -/** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. - * - *

- * More formally, if this map contains a mapping from a key {@code k} to a - * value {@code v} such that {@code (key==null ? k==null : - * key.equals(k))}, then this method returns {@code v}; otherwise it returns - * {@code null}. (There can be at most one such mapping.) - * - *

- * A return value of {@code null} does not necessarily indicate that - * the map contains no mapping for the key; it's also possible that the map - * explicitly maps the key to {@code null}. The {@link #containsKey - * containsKey} operation may be used to distinguish these two cases. - * - * @see #put(Object, Object) - */ -public V get(Object key) { - // 如果key是null,table[0]这个位置的元素将被返回。 - if (key == null) - return getForNullKey(); - // 计算hash值 - int hash = hash(key.hashCode()); - // indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。 - for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { - Object k; - // 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。 - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) - return e.value; - } - return null; -} -``` - -总结: - -- `HashMap`有一个叫做`Entry`的内部类,它用来存储`key-value`对。 -- 上面的`Entry`对象是存储在一个叫做`table`的`Entry`数组中。 -- `table`的索引在逻辑上叫做“桶”`(bucket)`,它存储了链表的第一个元素。 -- `key`的`hashcode()`方法用来找到`Entry`对象所在的桶。 -- 如果两个`key`有相同的`hash`值,他们会被放在`table`数组的同一个桶里面。 -- `key`的`equals()`方法用来确保`key`的唯一性。 -- `value`对象的`equals()`和`hashcode()`方法根本一点用也没有。 - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - diff --git "a/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" "b/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" deleted file mode 100644 index 03e7f1cd..00000000 --- "a/Java\345\237\272\347\241\200/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" +++ /dev/null @@ -1,115 +0,0 @@ -JVM垃圾回收机制 -=== - -引用计数算法 ---- - -在`JDK1.2`之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区, -堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象, -同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1, -而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候, -标志着这个对象已经没有引用了,可以回收了! -这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题,那就是互相引用的问题: -```java -ObjA.obj = ObjB -ObjB.obj - ObjA -``` -这样的代码会产生如下引用情形`objA`指向`objB`,而`objB`又指向`objA`,这样当其他所有的引用都消失了之后, -`objA`和`objB`还有一个相互的引用,也就是说两个对象的引用计数器各为1, -而实际上这两个对象都已经没有额外的引用,已经是垃圾了。 - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/yinyongjishu.jpg) - -根搜索算法 ---- - -根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图, -从一个节点`GC ROOT`开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点, -当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/genshousuo.jpg) - -目前java中可作为GC Root的对象有: - -- 虚拟机栈中引用的对象(本地变量表) -- 方法区中静态属性引用的对象 -- 方法区中常量引用的对象 -- 本地方法栈中引用的对象(Native对象) - -垃圾回收算法 ---- - -而手机后的垃圾是通过什么算法来回收的呢: - -- 标记-清除算法 -- 复制算法 -- 标记整理算法 - -那我们就继续分析下这三种算法: - -- 标记-清除算法(Mark-Sweep) - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_qingchu.jpg) - 标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记 的对象,进行回收,如上图所示。 - 标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片! - -- 复制算法(Copying) - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/fuzhisuanfa.jpg) - - 复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。 - -- 标记-整理算法(Mark-Compact) - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_zhengli.jpg) - - 整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。 - -- 分代收集算法(Generational Collection) - 分代收集算法是目前大部分`JVM`的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。 - 一般情况下将堆区划分为老年代(`Tenured Generation`)和新生代(`Young Generation`),老年代的特点是每次垃圾收集时只有少量对象需要被回收, - 而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。 -  目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少, - 但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的`Eden`空间和两块较小的`Survivor`空间, - 每次使用`Eden`空间和其中的一块`Survivor`空间,当进行回收时,将`Eden`和`Survivor`中还存活的对象复制到另一块`Survivor`空间中, - 然后清理掉`Eden`和刚才使用过的`Survivor`空间。 - -  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法。 -  注意,在堆区之外还有一个代就是永久代(`Permanet Generation`),它用来存储`class`类、常量、方法描述等。对永久代的回收主要回收两部分内容: - 废弃常量和无用的类。 - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/xinshengdai.jpg) - 对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的`Eden Space`和`From Space`, - 少数情况下会直接分配在老年代。如果新生代的`Eden Space`和`From Space`的空间不足,则会发起一次`GC`,如果进行了`GC`之后,`Eden Space`和`From Space` - 能够容纳该对象就放在`Eden Space`和`From Space`。在`GC`的过程中,会将`Eden Space`和`From Space`中的存活对象移动到`To Space`, - 然后将`Eden Space`和`From Space`进行清理。如果在清理的过程中,`To Space`无法足够来存储某个对象,就会将该对象移动到老年代中。 - 在进行了`GC之`后,使用的便是`Eden space`和`To Space`了,下次`GC`时会将存活对象复制到`From Space`,如此反复循环。 - 当对象在`Survivor`区躲过一次`GC`的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。 - -垃圾收集器 ---- - -垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。 -下面介绍一下`HotSpot(JDK 7)`虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。 -- Serial/Serial Old - `Serial/Serial Old`收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时, - 必须暂停所有用户线程。`Serial`收集器是针对新生代的收集器,采用的是`Copying`算法,`Serial Old`收集器是针对老年代的收集器, - 采用的是`Mark-Compact`算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。 - -- ParNew - `ParNew`收集器是`Serial`收集器的多线程版本,使用多个线程进行垃圾收集。 - -- Parallel Scavenge - `Parallel Scavenge`收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是`Copying`算法, - 该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。 - -- Parallel Old - `Parallel Old`是`Parallel Scavenge`收集器的老年代版本(并行收集器),使用多线程和`Mark-Compact`算法。 - -- CMS - `CMS(Current Mark Sweep)`收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是`Mark-Sweep`算法。 - -- G1 - `G1`收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多`CPU`、多核环境。因此它是一款并行与并发收集器, - 并且它能建立可预测的停顿时间模型。 - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" "b/Java\345\237\272\347\241\200/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" deleted file mode 100644 index 5af436ea..00000000 --- "a/Java\345\237\272\347\241\200/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" +++ /dev/null @@ -1,51 +0,0 @@ -Vim使用教程 -=== - -`Better, Stronger, Faster` - -首先`Vim`有两种模式: - -- `Normal` - 该模式下不能写入,修改要在该模式进行。在`Insert`模式中可以使用`ESC`键来返回到`Normal`模式。 -- `Insert` - 该模式下可以进行写入。在`Normal`模式下使用按`i`键进行`Insert`模式。 - -下面说一下`Normal`状态下的一些命令,所有的命令都要在`Normal`状态下执行: - -- `i` 进入`insert`模式 -- `x` 删除光标所在位置的一个字符 -- `dd` 删除当前行,并把删除的行存到剪贴板中 -- `p` 粘贴 -- `yy` 拷贝当前行 -- `a` 在光标后进行插入,直接进入`Insert`模式 -- `o` 在当前行后插入一个新行,直接进入`Insert`模式 -- `O` 在当前行钱插入一个新行,直接进入`Insert`模式 -- `cw` 替换从光标位置开始到该单词结束位置的所有字符,直接进入`Insert`模式 -- `u` 撤销、回退 -- `Ctrl+r` 重新添加、前行 -- `y` 拷贝选中部分内容 -- `y$` 拷贝光标至本行结束位置 - -- `:wq` 保存并退出 -- `:w` 保存 -- `:q!` 退出不保存 -- `:saveas ` 另存为 -- `:e filename` 打开文件 -- `:sav filename` 保存为某文件名 - -- `e` 移动到单词结束位置 -- `b` 移动到单词开始位置 -- `0` 数字0光标移动当行头 -- `gg` 移动到文件开始位置 -- `L` 移动到文件结束位置 -- `G` 移动到当前行结束位置 -- `:59` 移动到59行 - - - - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - diff --git "a/Java\345\237\272\347\241\200/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" "b/Java\345\237\272\347\241\200/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" deleted file mode 100644 index 33b473ea..00000000 --- "a/Java\345\237\272\347\241\200/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" +++ /dev/null @@ -1,27 +0,0 @@ -volatile和Synchronized区别 -=== - -- volatile - `Java`语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量 -的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而`volatile`关键字就是提示`JVM`:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。 -使用建议:在两个或者更多的线程访问的成员变量上使用`volatile`。当要访问的变量已在`synchronized`代码块中,或者为常量时,不必使用。 -由于使用`volatile`屏蔽掉了`JVM`中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 就跟`C`中的一样 禁止编译器进行优化. - - 注意:如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对`long`和`double`的简单操作之外,`volatile`并不能提供原子性。 -所以,就算你将一个变量修饰为`volatile`,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生! - -- synchronized - `synchronized`为一段操作或内存进行加锁,它具有互斥性。当线程要操作被`synchronized`修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。 - 它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。 - - 当两个并发线程访问同一个对象中的这个`synchronized(this)`同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 - - 然而,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,另一个线程仍然可以访问该`object`中的非`synchronized(this)`同步代码块。 - - 尤其关键的是,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,其他线程对`object`中所有其它`synchronized(this)`同步代码块的访问将被阻塞。 - -- 区别: - - `volatile`是变量修饰符,而`synchronized`则作用于一段代码或方法。 - - `volatile`只是在线程内存和“主”内存间同步某个变量的值;而`synchronized`通过锁定和解锁某个监视器同步所有变量的值。显然`synchronized`要比`volatile`消耗更多资源。 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/\345\215\225\351\223\276\350\241\250.md" "b/Java\345\237\272\347\241\200/\345\215\225\351\223\276\350\241\250.md" deleted file mode 100644 index 8108244a..00000000 --- "a/Java\345\237\272\347\241\200/\345\215\225\351\223\276\350\241\250.md" +++ /dev/null @@ -1,28 +0,0 @@ -单链表 -=== - -链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 -链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。 -每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,链表比较方便插入和删除操作。 - -用一组地址任意的存储单元存放线性表中的数据元素。以元素(数据元素的映象) + 指针(指示后继元素存储位置) = 结点。 -以“结点的序列”表示线性表,称作线性链表(单链表)。单链表是一种顺序存取的结构,为找第 i 个数据元素,必须先找到第 i-1 个数据元素。 -链表的结点结构:  - ┌──┬──┐──┐ - │data│next│ - └──┴──┘──┘ - data域:存放结点值的数据域    - next域:存放结点的直接后继的地址(位置)的指针域(链域)。 - 注意:①链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。    - ②每个结点只有一个链域的链表称为单链表(Single Linked List)。 -所谓的链表就好像火车车厢一样,从火车头开始,每一节车厢之后都连着后一节车厢。 - -和数组相比,链表的优势在于长度不受限制,并且在进行插入和删除操作时,不需要移动数据项,故尽管某些操作的时间复杂度与数组想同,实际效率上还是比数组要高很多 - -劣势在于随机访问,无法像数组那样直接通过下标找到特定的数据项 - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/Java\345\237\272\347\241\200/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" "b/Java\345\237\272\347\241\200/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" deleted file mode 100644 index c7348eba..00000000 --- "a/Java\345\237\272\347\241\200/\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" +++ /dev/null @@ -1,73 +0,0 @@ -原子性、可见性以及有序性 -=== - -- 原子性: - 众所周知,原子是构成物质的基本单位,所以原子代表着不可分。 - 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 - 最简单的一个例子就是银行转账问题,赋值或者`return`。比如`a = 1;`和 `return a;`这样的操作都具有原子性 - 原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作! - 加锁可以保证复合语句的原子性,`sychronized`可以保证多条语句在`synchronized`块中语意上是原子的。 - -- 可见性: - 在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时, - 多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。 - (这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数);同样在单核处理器中这样由于备份造成的问题同样存在! - 这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作覆盖掉了, - 这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 - -- 非原子性操作 - 类似`a += b`这样的操作不具有原子性,在某些`JVM`中`a += b`可能要经过这样三个步骤: - - 取出`a`和`b` - - 计算`a+b` - - 将计算结果写入内存 - 如果有两个线程`t1`,`t2`在进行这样的操作。`t1`在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是`t2`开始执行,`t2`执行完毕后`t1`又把没有完成的第三步做完。这个时候就出现了错误, - 相当于`t2`的计算结果被无视掉了。所以上面的买碘片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 - 类似的,像`a++`这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。 - -- 有序性 - 有序性:即程序执行的顺序按照代码的先后顺序执行。 - ```java - int i = 0; - boolean flag = false; - i = 1; //语句1 - flag = true; //语句2 - ``` - 上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? - 不一定,为什么呢?这里可能会发生指令重排序`(Instruction Reorder)`。 -  下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 -  比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。 -  但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢? - 再看下面一个例子: - ```java - int a = 10; //语句1 - int r = 2; //语句2 - a = a + 3; //语句3 - r = a*a; //语句4 - ``` - 这段代码有4个语句,那么可能的一个执行顺序是: - 语句2->语句1->语句3->语句4 - 那么可能不可能是这个执行顺序呢?语句2->语句1->语句4->语句3,这是不可能的,因为处理器在进行重排序时是会考虑指令之间的数据依赖性, - 如果一个指令`Instruction 2`必须用到`Instruction 1`的结果,那么处理器会保证`Instruction 1`会在`Instruction 2`之前执行。 - - 虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子: - ```java - //线程1: - context = loadContext(); //语句1 - inited = true; //语句2 - - //线程2: - while(!inited ){ - sleep() - } - doSomethingwithconfig(context); - ``` - 上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成, - 那么就会跳出`while`循环,去执行`doSomethingwithconfig(context)`方法,而此时`context`并没有被初始化,就会导致程序出错。 - 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 -  也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - diff --git "a/Java\345\237\272\347\241\200/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" "b/Java\345\237\272\347\241\200/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" deleted file mode 100644 index f0bdd905..00000000 --- "a/Java\345\237\272\347\241\200/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" +++ /dev/null @@ -1,61 +0,0 @@ -常用命令行大全 -=== - -- 创建文件夹 - `mkdir test` - -- 进入文件夹 - `cd test` - -- 显示路径 - 在需要显示的文件夹中执行 `pwd` - -- 创建空文件 - `touch fileName` - -- 查看文本内容 - `cat xxx.txt` - -- 删除文件 - `rm xxx.txt` - `rm -rf file`// -fr表示递归和强制,一定要小心使用 rm -fr / 那你的电脑就over了 - -- 拷贝 - `cp -R 源文件 目标文件`// -R 表示对目录进行递归操作 - -- 移动 - `mv filePath toPath` - -- 显示当前目录的路径名 - 在目录中执行`pwd` - -- 查看文件内容 - `cat fileName` - -- 清楚命令行内容 - `clear` - -- 显示日期 - `date` - -- 显示文件 - `ls` - `ls -a`// 显示隐藏文件 - `ls -l`列出详细信息 - -- 关机 - `sudo shutdown -h now`// -h 是关闭电源 now立即关机 - `sudo shutdown -r now`//重启 - `sudo shutdown -h -time 60`// 表示60分钟后关机,注意单位是分钟 - - - - - - - ----- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" "b/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" deleted file mode 100644 index a8d8ccfc..00000000 --- "a/Java\345\237\272\347\241\200/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" +++ /dev/null @@ -1,40 +0,0 @@ -强引用、软引用、弱引用、虚引用 -=== - -- 强引用(Strong Reference) - 平时我们编程的时候例如:`Object object=new Object()`;那`object`就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品, - 垃圾回收器绝不会回收它。当内存空 间不足,`Java`虚拟机宁愿抛出`OutOfMemoryError`错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 - -- 软引用(SoftReference) - 如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。 - 只要垃圾回收器没有回收它,该对象就可以被程序使用。**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用, - 如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 - -- 弱引用(WeakReference) - 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 - 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器 - 弱引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果弱引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 - 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器 - -- 虚引用(PhantomReference) - "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用, - 那么它就和没有任何引用一样,在 任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于: - 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前, - 把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 - 程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 - 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 - 虚引用主要用于检测对象是否已经从内存中删除。 - - -有关弱引用以及软引用再分析一下: -我们都知道垃圾回收器会回收符合回收条件的对象的内存,但并不是所有的程序员都知道回收条件取决于指向该对象的引用类型。这正是`Java`中弱引用和软引用的主要区别。 -如果一个对象只有弱引用指向它,垃圾回收器会立即回收该对象,这是一种急切回收方式。相对的,如果有软引用指向这些对象,则只有在`JVM`需要内存时才回收这些对象。 -弱引用和软引用的特殊行为使得它们在某些情况下非常有用。例如:软引用可以很好的用来实现缓存,当`JVM`需要内存时,垃圾回收器就会回收这些只有被软引用指向的对象。 -而弱引用非常适合存储元数据,例如:存储`ClassLoader`引用。如果没有类被加载,那么也没有指向`ClassLoader`的引用。一旦上一次的强引用被去除, -只有弱引用的`ClassLoader`就会被回收。 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/\347\256\227\346\263\225.md" "b/Java\345\237\272\347\241\200/\347\256\227\346\263\225.md" deleted file mode 100644 index 2c652746..00000000 --- "a/Java\345\237\272\347\241\200/\347\256\227\346\263\225.md" +++ /dev/null @@ -1,63 +0,0 @@ -算法 -=== - -算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题, -执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。 -算法可以理解为有基本运算及规定的运算顺序所构成的完整的解题步骤。或者看成按照要求设计好的有限的确切的计算序列,并且这样的步骤和序列可以解决一类问题。 -```java -/** - * 1到100所有素数的和 - * 素数就是只能够被1和自身整除的数 - */ -public class dd { - public static void main(String[] args) { - int i = 2; // i 即为所求素数 - System.out.println("i= " + i); - for (i = 3; i <= 100; i = i + 2) { - boolean f = true; - Label: for (int j = 2; j < i; j++) { - if (i % j == 0) { // if(true)时,i为非素数 - f = false; - break Label; // 加了Label貌似只是起到提高效率 - } - } - if (f) {// 当f=true时,i为素数。 - System.out.println("i= " + i); - } - } - } -} - -/** - *古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死, - *问每个月的兔子总数为多少? - * - */ -public class aa{ - public static void main(String[] args) { - int i = 0; - for (i = 1; i < 20; i++) { - System.out.println(f(i)); - } - } - - public static int f(int x) { - if(x==1||x==2) { - return 1; - }else { - return f(x-1)+f(x-2); - } - } -} -``` - -3.二叉树 -4. -数据结构分为哪两种结构? -线性结构:线性表、栈、队列、串、?/span> -非线性结构:树、图、 - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/Java\345\237\272\347\241\200/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" "b/Java\345\237\272\347\241\200/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" deleted file mode 100644 index 7206fb29..00000000 --- "a/Java\345\237\272\347\241\200/\347\272\277\347\250\213\346\261\240\347\232\204\345\216\237\347\220\206.md" +++ /dev/null @@ -1,48 +0,0 @@ -线程池的原理 -=== - -- 在什么情况下使用线程池? - - 单个任务处理的时间比较短 - - 将需处理的任务的数量大 - -- 使用线程池的好处: - - 减少在创建和销毁线程上所花的时间以及系统资源的开销 - - 如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 - - 提交相应速度 - -- 工作流程 - 线程池的任务就在于负责这些线程的创建,销毁和任务处理参数传递、唤醒和等待。 - - 创建若干线程,置入线程池 - - 任务达到时,从线程池取空闲线程 - - 取得了空闲线程,立即进行任务处理 - - 否则新建一个线程,并置入线程池,执行3 - - 如果创建失败或者线程池已满,根据设计策略选择返回错误或将任务置入处理队列,等待处理 - - 销毁线程池 - -- 风险 - 虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。 - 用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁, - 它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。 - -- 资源不足 - 线程池的一个优点在于:相对于其它替代调度机制而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。 - 线程消耗包括内存和其它系统资源在内的大量资源。除了`Thread`对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。 - 除此以外`JVM`可能会为每个`Java`线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后虽然线程之间切换的调度开销很小, - 但如果有很多线程,环境切换也可能严重地影响程序的性能。 - 如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间, - 而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。 - 除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如`JDBC`连接、套接字或文件。 - 这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配`JDBC`连接。 - -- 线程池的关闭 - 我们可以通过调用线程池的`shutdown`或`shutdownNow`方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的`interrupt`方法来中断线程,所以无法响应中断的任务可能永远无法终止。 - 但是它们存在一定的区别,`shutdownNow`首先将线程池的状态设置成`STOP`,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表, - 而`shutdown`只是将线程池的状态设置成`SHUTDOWN`状态,然后中断所有没有正在执行任务的线程。 - 只要调用了这两个关闭方法的其中一个,`isShutdown`方法就会返回`true`。当所有的任务都已关闭后,才表示线程池关闭成功, - 这时调用`isTerminaed`方法会返回`true`。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用`shutdown`来关闭线程池,如果任务不一定要执行完,则可以调用`shutdownNow`。 - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - diff --git "a/Jetpack/Jetpack\347\256\200\344\273\213.md" "b/Jetpack/Jetpack\347\256\200\344\273\213.md" new file mode 100644 index 00000000..e29d72c2 --- /dev/null +++ "b/Jetpack/Jetpack\347\256\200\344\273\213.md" @@ -0,0 +1,161 @@ +# Jetpack简介 + +JetPack是Google推出的一些库的集合。是Android基础支持库SDK以外的部分。包含了组件、工具、架构方案等...开发者可以自主按需选择接入具体的哪个库。 + +## 背景 + +### Support库 + +早之前的Android更新迭代是,所有的功能更新都是跟随着每一个特定的Android版本所发布的。 +例如: +- Fragment是在Android 3.0更新的。 +- Material Design组件是在Android 5.0上更新的 +但是由于Android用户庞大,每一次Android更新都无法覆盖所有用户的,同时因为手机厂商众多,但支持有限,也无法为自己生产的所有设备保持迭代到最新的Android版本,所以用户所持有的设备上Android版本是层次不齐的。 +从技术的角度来说,因为用户的设备版本不一致,导致Android工程师在维护项目的时候,会遇到很多难以解决的问题。为了解决这些由于Android版本不一致而出现的兼容性问题,Google推出了Support库。 + +Support库是针对Framework API的补充,Framework API跟随每一个Android版本所发布,和设备具有强关联性,Support API是开发者自主集成的,最终会包含在我们所发布的应用中,这样我们就可以在最新的Android版本上进行应用的开发,同时使用Support API解决各种潜在的兼容性问题,帮助开发者在不同Android版本的设备上实现行为一致的工作代码。 + +### Support 库的弊端 +最早的Support库发布于2011年,版本号为:android.support.v4,也就是我们所熟知的v4库,2013年在v4的基础上,Android团队发布了v7库,版本号为:android.support.v7,之后还发布了用于特定场景的v8、v13、v14、v17。 +如果是前几年刚开始学习Android的同学们,一定都对这些奇怪的数字很疑惑,4、7、8、13、14、17 到底都是什么意思? +拿第一代支持库v4举例,最初本意是指:该支持库最低可以支持到API 4(Android 1.4)的设备,v7表示最低支持 API 7(Android 2.1)的设备,但随着Android版本的持续更新,API 4以及API 7的设备早就淘汰了。在2017年7月Google将所有支持库的最低API支持版本提高到了API 14(Android 4.0),但由于包名无法修改,所以还是沿用之前的v4、v7命名标准,所以就出现了Support库第一个无法解决的问题:版本增长混乱。 + +与此同时Support库还面临一个非常严峻的问题:架构设计本身导致的严重依赖问题。最早的Support库是v4,v7是基于v4进行的补充,因为v4、v7太过庞大,功能集中,所以如果想要开发新的支持库,也只能在v4、v7的基础上进行二次开发,比方说我们后期常用的,RecyclerView、CardView等等。 + +这样就会产生很严重的重复依赖的问题,在无论是使用官方库,还是第三方库的时候,我们都需要保持整个项目中Support库版本一致,我相信很多人都在这个问题上踩过坑,虽然还是有办法解决这个问题,但无形中增加了很多工作量。 + +我们都知道“组合优于继承”这句话,但Support库在最初的架构设计上,却采用了重继承轻组合的方式,我猜这可能是因为开发Support库的人和开发Framework API的是同一批人有关,Framework API里有种各种继承逻辑,例如我们常用的 Activity、Fragment、View。 + +虽然在后期Google尝试拆分Support库,例如推出了独立的支持库:support:design、support:customtabs等,但并不能从根源解决依赖的问题。 + +### Android X +从Goole IO 2017开始。Google开始推出Architecture Component,ORM库Room,用户生命周期管理的ViewModel/LiveData. +Goole IO 2018将Support lib更名为androidx.将许多Google认为是正确的方案和实践集中起来。可以说AndroidX的出现就是为了解决长久以来Support库混乱的问题,你也可以把AndroidX理解为更强大的Support库。 +AndroidX将原有的Support库拆分为85个大大小小的支持库,抛弃了之前与API最低支持相关联的版本命名规范,重置为1.0.0,并且每一个库在之后都会按照严格的语义版本控制规则进行版本控制。 +同时通过组合依赖的方式,我们可以选择自己需要的组件库,而不是像Support一样全部依赖,一定程度上也减小了应用的体积。 +很重要的一点,就是它不会随着特定的Android版本而更新,它是由开发者自主控制,同时包含在我们所发布的应用程序中。 + + +以上种种,现在统称为JetPack. +Jetpack的出现是为了彻底解决这两个致命的问题: +1. Support 库版本增长混乱 +2. Support 库重复依赖 +如果Jetpack仅仅是针对Support库的重构,那它并没有了不起的,因为这只是Google解决了它自身因为历史原因所产生的代码问题。 +更重要的是Jetpack为大家提供了一系列的最佳实践,包含:架构、数据处理、后台任务处理、动画、UI 各个方面,无需纠结于各种因为Android本身而出现的问题,而是让我们把更多的精力放在业务需求的实现上,这才是Jetpack真正了不起的地方。其最核心的出发点就是帮助开发者快速构建出稳定、高性能、测试友好同时向后兼容的APP。 + +Jetpack相当于Google把自己的Android生态重新整理了一番。确立了Android未来的版图和大方向。 + +## 组成部分 + +前面讲到过,JetPack是一系列库和工具的集合,它更多是Google的一个提出的一个概念,或者说态度。 +并非所有的东西都是每年在IO大会上新推出的,它也包含了对现有基础库的整理和扩展。在大部分项目中其实我们都有用到JetPack的内容,也许你只是不知道而已。让我们以上帝视角来看看整个JetPack除了你熟悉的部分,还有哪些是你不熟悉但是听过的内容。看看他们都能做些什么事情。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_jetpack.png) + +[Jetpack完整组件列表](https://developer.android.com/jetpack/androidx/explorer) + +### Foundation(基础组件): +基础组件提供了横向功能,例如向后兼容性、测试以及Kotlin语言的支持。它包含如下组件库: +- Android KTX:Android KTX 是一组 Kotlin 扩展程序,它优化了供Kotlin使用的Jetpack和Android平台的API。以更简洁、更愉悦、更惯用的方式使用Kotlin进行Android开发。 +- AppCompat:提供了一系列以AppCompat开头的API,以便兼容低版本的Android开发。Jetpack基础中的AppCompat库包含v7库中的所有组件([支持库软件包](https://developer.android.com/topic/libraries/support-library/packages#v7-appcompat))。 其中包括AppCompat,Cardview,GridLayout,MediaRouter,Palette,RecyclerView,Renderscript,Preferences,Leanback,Vector Drawable,Design,Custom选项卡等。此外,该库为材质设计用户界面提供了实现支持,这使得AppCompat对 开发人员。 以下是android应用程序的一些关键领域,这些领域很难构建,但是可以使用AppCompat库轻松进行设计: 一般都是为了兼容 Android L以下版本,来提供Material Design的效果: + - Toolbar + - ContextCompat + - AppCompatDialog +- annotation:注解,提升代码可读性,内置了Android中常用的注解 +- Multidex(多Dex处理):为方法数超过 64K 的应用启用多 dex 文件。Security(安全):按照安全最佳做法读写加密文件和共享偏好设置。 +- Test(测试):用于单元和运行时界面测试的 Android 测试框架。 + +### Architecture(架构组件) +架构组件可帮助开发者设计稳健、可测试且易维护的应用。它包含如下组件库: +- Data Binding(数据绑定):数据绑定库是一种支持库,借助该库,可以使用声明式将布局中的界面组件绑定到应用中的数据源。 +- Lifecycles:方便管理Activity和Fragment生命周期,帮助开发者书写更轻量、易于维护的代码。 +- ViewModel:以生命周期感知的方式存储和管理与UI相关的数据。 +- LiveData:是一个可观察的数据持有者类。与常规observable不同,LiveData是有生命周期感知的。 +- Navigation:处理应用内导航所需的一切。 +- Paging:帮助开发者一次加载和显示小块数据。按需加载部分数据可减少网络带宽和系统资源的使用。 +- Room:Room持久性库在SQLite上提供了一个抽象层,帮助开发者更友好、流畅的访问SQLite数据库。 +- WorkManager:即使应用程序退出或设备重新启动,也可以轻松地调度预期将要运行的可延迟异步任务。 +- hilt:基于Dagger的Android依赖注入框架绑定View和Model +- startup:自动处理依赖初始化 +- datastore:Preferences的替代类,支持异步、更加安全 + + + +### Behavior(行为组件) +行为组件可帮助开发者的应用与标准Android服务(如通知、权限、分享和Google助理)相集成。它包含如下组件库: +- CameraX:帮助开发者简化相机应用的开发工作。它提供一致且易于使用的 API 界面,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。 +- DownloadManager下载管理器:可处理长时间运行的HTTP下载,并在出现故障或在连接更改和系统重新启动后重试下载。 +- Media & playback(媒体&播放):用于媒体播放和路由(包括 Google Cast)的向后兼容 API。 +- Notifications(通知):提供向后兼容的通知 API,支持 Wear 和 Auto。 +- Permissions(权限):用于检查和请求应用权限的兼容性 API。 +- Preferences(偏好设置):提供了用户能够改变应用的功能和行为能力。 +- Sharing(共享):提供适合应用操作栏的共享操作。 +- Slices(切片):创建可在应用外部显示应用数据的灵活界面元素。 + +### UI(界面组件) +大多数的UI组件其实都包含在基础组件中的appcompat中,这里是一些独立组件库存在的UI组件,界面组件可提供各类view和辅助程序,让应用不仅简单易用,还能带来愉悦体验。它包含如下组件库: +- drawerlayout:抽屉布局 +- recyclerview:可复用的滑动列表 +- constraintlayout:约束布局 +- compose*: Jetpack compose声明式UI +- coordinatorlayout:顶层布局继承自Framelayout,可以实现子View之间的联动交互效果 +- swiperefreshlayout:下拉刷新布局 +- viewpager2:分页布局 +- Material Design Components * : MD组件 +- Animation & Transitions(动画&过度):提供各类内置动画,也可以自定义动画效果。 +- Emoji(表情符号):使用户在未更新系统版本的情况下也可以使用表情符号。 + - EmojiTextView + - EmojiEditTExt + - EmojiButton +- Fragment:组件化界面的基本单位。 +- Layout(布局):xml书写的界面布局或者使用Compose完成的界面。 + 用户界面结构(如应用程序的活动)由Layout定义。 它定义了View和ViewGroup对象。 可以通过两种方式创建View和ViewGroup:通过以XML声明UI元素或通过编写代码(即以编程方式)。 Jetpack的这一部分涵盖了一些最常见的布局,例如LinearLayout,RelativeLayout和全新的ConstraintLayout。 而且,官方的Jetpack布局文档提供了一些指导,以使用RecyclerView创建项目列表以及使用CardView创建卡布局。 用户可以看到一个视图。 EditView,TextView和Button是View的示例。 另一方面,ViewGroup是一个容器对象,它定义了View的布局结构,因此它是不可见的。 ViewGroup的示例是LinearLayout,RelativeLayout和ConstraintLayout。 +- Palette(调色板):从调色板中提取出有用的信息。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jetpack_compose.png?raw=true) + +[Android Jetpack Compose](https://developer.android.com/jetpack/compose)是2019 Google/IO大会上推出的一种声明式的UI开发框架,经过一年左右的演进,现在到了alpha阶段。Jetpack Compose是用于构建原生界面的新款Android工具包。它可简化并加快Android上的界面开发。使用更少的代码、强大的工具和直观的KotlinAPI,快速让应用生动而精彩,从此不再需要写xml,使用声明式的Compose函数来构建页面UI。 + +Compose由androidx中的6个Maven组ID构成。每个组都包含一套特定用途的功能,并各有专属的版本说明。下表介绍了各个组及指向其版本说明的链接: +- compose.animation:在Jetpack Compose应用中构建动画,丰富用户的体验。 +- compose.compiler:借助Kotlin编译器插件,转换@Composable functions(可组合函数)并启用优化功能。 +- compose.foundation:使用现成可用的构建块编写Jetpack Compose应用,还可扩展Foundation以构建您自己的设计系统元素。 +- compose.material:使用现成可用的Material Design组件构建Jetpack Compose UI。这是更高层级的Compose入口点,旨在提供与www.material.io上描述的组件一致的组件。 +- compose.runtime:Compose的编程模型和状态管理的基本构建块,以及Compose编译器插件针对的核心运行时。 +- compose.ui:与设备互动所需的Compose UI的基本组件,包括布局、绘图和输入。 +```kotlin +class MainActivity : AppCompatActivity() { + overridefun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Text("Hello, Android技术杂货铺") + } + } +} +``` +废弃部分: +- asynclayoutinflater:异步生成UI +- localbroadcastmanager:本地广播 +- viewpager:使用vviewpager2 +- cardview:使用MaterialCardView替代 + + +## Jetpack的意义 +Jetpack里目前包含的内容,未来也会是Google大力维护和扩展的内容。对应开发者来说也是值得去学习使用的且相对无后顾之忧的。JetPack里没有的,除开一些优秀的第三方库,未来应该也会慢慢被新的API替代,逐渐边缘化,直至打上Deprecate注解。 + +以当下的环境来说,要开发出一个完全摆脱JetPack的APP是很难做到的。但是反过来讲JetPack也远远没有到成熟的地步,目前也还存在亟待解决的问题,未来可以做的事情还有很多。 + +关于使用的话,并不是所有库都建议使用,因为目前还有很多库在alpha版本。但是作为学习还是很有必要的,能给你日常的开发中多提供一些思路,这些是无可厚非的。 + + + + + +## Jetpack库列表及集成 + +[Jetpack各库版本及gradle集成使用](https://developer.android.com/jetpack/androidx/releases/activity) + + + +## 参考 +- [Jetpack Compose初体验 ](https://easyliu-ly.github.io/2020/12/12/android_jetpack/compose/) diff --git "a/Jetpack/architecture/1.\347\256\200\344\273\213.md" "b/Jetpack/architecture/1.\347\256\200\344\273\213.md" new file mode 100644 index 00000000..3bf0bd85 --- /dev/null +++ "b/Jetpack/architecture/1.\347\256\200\344\273\213.md" @@ -0,0 +1,134 @@ +1.简介 +=== + +应用开发者面临的常见问题 +--- + +在大多数情况下,桌面应用程序在启动器快捷方式中有一个单一的入口并且作为单独的独立进程运行,与桌面应用程序不同的是`Android`应用具有更复杂的结构。一个典型的`Android`应用是由多个应用程序组件构成的,包括`activity`,`fragment`,`service`,`content provider`和`broadcast receiver`。 +这些应用程序组件中的大部分声明在`manifest.xml`文件中,用来决定如何将应用融入到用户设备的整体体验中。尽管如前所述,传统的桌面应用程序作为独立进程运行,但是正确的编写`Android`应用程序需要更加灵活,因为用户会同过设备上不同的应用程序组织成自己的方式不断切换流程和任务。 +例如,考虑下在你喜欢的社交网络应用中分享照片时会发生什么。该应用会触发一个启动相机的`intent`,从该`intent`会启动一个相机应用来处理这个请求。在此刻,用户离开社交网络应用但是用户的体验是无缝的。相机应用转而可能会触发其它的`intent`例如启动文件选择器,这可能会启动另一个应用。最终用户回到社交网络应用并且分享照片。此外,在这个过程中的任何时刻用户都有可能会被一个电话打断,并且在结束通话后再回来继续分享照片。 +在`Android`中,这种应用切换行为很常见,所以你的应用程序必须正确处理这些流程。记住,移动设备的资源是有限的,所以在任何时候,操作系统都可能会杀死一些应用为新的应用腾出空间。 +其中的重点是应用程序组件可能会被单独和无序的启动,并且可能会被用户或系统在任何时候销毁。因为应用程序组件是短暂的,并且其声明周期(什么时候被创建和销毁)不受你控制,所以不应该在应用程序组件中存储任何应用数据或状态,同时应用程序组件不应该相互依赖。 + +通用的框架准则官方建议在架构`App`的时候遵循以下两个准则: + +- 关注分离 + + 其中早期开发`App`最常见的做法是在`Activity`或者`Fragment`中写了大量的逻辑代码,导致`Activity`或`Fragment`中的代码很臃肿,十分不易维护。现在很多`App` 开发者都注意到了这个问题,所以前两年`MVP`结构就非常有市场,目前普及率也很高。 + +- 模型驱动`UI` + + 模型持久化的好处就是:即使系统回收了`App`的资源用户也不会丢失数据,而且在网络不稳定的情况下`App`依然可以正常地运行。从而保证了`App`的用户体验。 + + +面对越来越复杂的`App`需求,`Google`官方发布了`Android`框架组件库`(Android Architecture Components)`使`App`的架构更加健壮。 + + +[Android Architecture Components](https://developer.android.com/topic/libraries/architecture/) +[googlesamples/android-architecture-components](https://github.com/googlesamples/android-architecture-components) + +> A collection of libraries that help you design robust, testable, and maintainable apps. Start with classes for managing your UI component lifecycle and handling data persistence. + + +`Android Architecture Components`,简称`AAC`意思就是一个处理`UI`的生命周期 与数据的持久化的架构 + +- 一个全新的库集合,可帮助您设计强大,可测试和可维护的应用程序。用于管理`UI`组件生命周期和处理数据持久性。 +- 便捷管理`App`的声明周期:新的生命周期感知(`lifecycle-aware`)组件可帮助您管理`Activity`和`Fragment`的生命周期。存储配置改变,避免内存泄漏,并使用`LiveData`,`ViewModel`,`LifecycleObserver`和`LifecycleOwner`轻松将数据加载到UI中。 +- `Room`:一个`SQLite`对象映射库 + + +平时我们比较熟悉的的架构有`MVC`,`MVP`及`MVVM`.这些结构有各自的优缺点, 以现在比较流行的`MVP`为例, 它将不是关于界面的操作分发到`Presenter`中操作,再将结果通知给`View`接口的实现(通常是`Activity/Fragment`). +`MVP`架构,当异步获取结果时,可能`UI`已经销毁,而`Presenter`还持有`UI`的引用,从而导致内存泄漏 + + +```kotlin +fun getName() { + ExecutorServiceManager.getInstance().execute(Runnable { + try { + TimeUnit.SECONDS.sleep(5) + } catch (e: InterruptedException) { + e.printStackTrace() + } + + if (iView != null) { + iView.setName("siyehua") + } + }) +} +``` + +以上代码,`iView`的具体实现是`Activity`,当`Activity`已经执行了`onDestroy()`方法,此时`Runnable`还在获取数据,就会导致内存泄漏. +通常的做法是在给`Presenter`定义一个方法,当`Activity`销毁的时候同时移除对`iView`的引用,完整代码如下: +```kotlin +class PersonPresenter(private var iView: IView?) : IPresenter { + fun getName() { + ExecutorServiceManager.getInstance().execute(Runnable { + try { + TimeUnit.SECONDS.sleep(5) + } catch (e: InterruptedException) { + e.printStackTrace() + } + + if (iView != null) { + iView!!.setName("siyehua") + } + }) + } + + fun removeView() { + iView = null + } +} +``` + +在`Activity`中,代码调用如下: + +```kotlin +override fun onDestroy() { + //不移除 View 有可能导致内存泄漏 + personPresenter.removeView() + super.onDestroy() +} +``` + +至此,即可解决`MVP`内存泄漏的问题,但是这么做不够优雅,需要手动管理`Presenter`,当然可以定义基类写入到`BaseActivity`中. +除了有可能引发内存泄漏的风险, 数据持久化也是一个经常困扰我们的问题.通常在屏幕旋转后,`UI`的对象都会被销毁重建,这将导致原来的对象数据不得不重新创建和获取,浪费资源的同时也会影响用户的体验. +通常的解决方法是,通过`SavedInstanceState`来存取数据,但`SavedInstanceState`存储的数据一般比较小,且数据对象还是必须重新构建. + +为了将代码解耦以应对日益膨胀的代码量,工程师在应用程序中引入了“架构”的概念。使之在不影响应用程序各模块组件间通信的同时,还能够保持模块的相对独立。这样不仅有利于后期维护,也有利于代码测试。 + + + +上述两个问题可以通过使用`AAC`架构解决. + + +`AAC`主要提供了`Lifecycle`,`ViewModel`,`LiveData`,`Room`等功能,下面依次说明: + +- `Lifecycle`:生命周期管理,把原先`Android`生命周期的中的代码抽取出来,如将原先需要在`onStart()`等生命周期中执行的代码分离到`Activity`或者`Fragment`之外。 +- `LiveData`:一个数据持有类,持有数据并且这个数据可以被观察被监听,和其他`Observer`不同的是,它是和`Lifecycle`是绑定的,在生命周期内使用有效,减少内存泄露和引用问题。 +- `ViewModel`:用于实现架构中的`ViewModel`,同时是与`Lifecycle`绑定的,使用者无需担心生命周期。可以在多个`Fragment`之间共享数据,比如旋转屏幕后`Activity`会重新`create`,这时候使用`ViewModel`还是之前的数据,不需要再次请求网络数据。 +- `Room`:谷歌推出的一个`Sqlite ORM`库,不过使用起来还不错,使用注解,极大简化数据库的操作,有点类似`Retrofit`的风格。 + + + + +- `Activity/Fragment` + `UI`层,通常是`Activity/Fragment`等,监听`ViewModel`,当`ViewModel`数据更新时刷新`UI`,监听用户事件反馈到`ViewModel`,主流的数据驱动界面。 + +- `ViewModel` + 持有或保存数据,向`Repository`中获取数据,响应`UI`层的事件,执行响应的操作,响应数据变化并通知到`UI`层。 + +- `Repository` + `App`的完全的数据模型,`ViewModel`交互的对象,提供简单的数据修改和获取的接口,配合好网络层数据的更新与本地持久化数据的更新,同步等 + +- `Data Source` + 包含本地的数据库等,网络`api`等,这些基本上和现有的一些`MVVM`,以及`Clean`架构的组合比较相似 + + + +- [下一篇:2.ViewBinding简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/2.ViewBinding%E7%AE%80%E4%BB%8B.md) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` diff --git "a/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" "b/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" new file mode 100644 index 00000000..f465ab8b --- /dev/null +++ "b/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" @@ -0,0 +1,360 @@ +# 10.DataStore简介 + +Jetpack DataStore是一种数据存储解决方案,允许您使用[协议缓冲区](https://developers.google.com/protocol-buffers)存储键值对或类型化对象。DataStore使用Kotlin协程和Flow以异步、一致的事务方式存储数据。 + +如果您当前在使用 [`SharedPreferences`](https://developer.android.com/reference/kotlin/android/content/SharedPreferences) 存储数据,请考虑迁移到DataStore。 + +**注意**:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 [Room](https://developer.android.com/training/data-storage/room),而不是 DataStore。DataStore非常适合简单的小型数据集,不支持部分更新或参照完整性。 + +## Preferences DataStore和Proto DataStore + +DataStore提供两种不同的实现:Preferences DataStore和Proto DataStore。 + +- **Preferences DataStore**像SharedPreferences一样,以键值对的形式进行基本类型的数据存储。DataStore基于Flow实现异步存储,避免因为阻塞主线程带来的ANR问题 +- **Proto DataStore**基于Protobuf实现任意自定义类型的数据存储,需要定义Protobuf的IDL,但是可以保证类型安全的访问。存储类的对象(typed objects ),通过protocol buffers将对象序列化存储在本地,protocol buffers现在已经应用的非常广泛,无论是微信还是阿里等等大厂都在使用。 + + + +## SharedPreferences的缺点 + +SharedPreference是一个轻量级的数据存储方式,使用起来也非常方便,以键值对的形式存储在本地,初始化SharedPreference的时候,会将整个文件内容加载内存中,因此会带来以下问题: + +- 通过`getXXX()`方法获取数据,可能会导致主线程阻塞 + + 所有getXXX()方法都是同步的,在主线程调用get方法,必须等待SharedPreference加载完毕,会导致主线程堵塞。 + +- SharedPreference不能保证类型安全 + + 调用getXXX()方法的时候,可能会出现ClassCastException异常,因为使用相同的key进行操作的时候,putXXX()方法可以使用不同类型的数据覆盖相同的key。 + +- SharedPreference 加载的数据会一直留在内存中,浪费内存 + + 通过getSharedPreferences()方法加载的数据,最后会将数据存储在静态的成员变量中。 + +- `apply()`方法虽然是异步的,但是可能会发生ANR,在8.0之前和8.0之后实现各不相同 + `apply()`方法是异步的,本身是不会有任何问题,但是当生命周期处于`handleStopService()`、`handlePauseActivity()`、`handleStopActivity()`的时候会一直等待`apply()`方法将数据保存成功,否则会一直等待,从而阻塞主线程造成ANR。 + +- `apply()`方法无法获取到操作成功或者失败的结果 + + `apply()`方法无法获取到操作成功或者失败的结果,而`commit()`方法是可以接收MemoryCommitResult里面的一个boolean参数作为结果 + +- SP不能用于跨进程通信 + + 我们在创建SP实例的时候,需要传入一个`mode`,如下所示: + + ``` + val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) + ``` + + Context内部还有一个`mode`是`MODE_MULTI_PROCESS`,我们来看一下这个`mode`做了什么 + + ``` + public SharedPreferences getSharedPreferences(File file, int mode) { + if ((mode & Context.MODE_MULTI_PROCESS) != 0 || + getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { + // 重新读取 SP 文件内容 + sp.startReloadIfChangedUnexpectedly(); + } + return sp; + } + ``` + + 在这里就做了一件事,当遇到`MODE_MULTI_PROCESS`的时候,会重新读取SP文件内容,并不能用SP来做跨进程通信。 + + + +## DataStore的优点 + +Preferences DataStore主要用来替换SharedPreferences,Preferences DataStore解决了SharedPreferences带来的所有问题 + +**Preferences DataStore相比于SharedPreferences优点: ** + +- DataStore是基于Flow实现的,所以保证了在主线程的安全性 +- 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性) +- 没有`apply()`和`commit()`等等数据持久的方法 +- 自动完成SharedPreferences迁移到DataStore,保证数据一致性,不会造成数据损坏 +- 可以监听到操作成功或者失败结果 + + + +如需在您的应用中使用Jetpack DataStore,请根据您要使用的实现向Gradle文件添加以下内容: + +```groovy +// Preferences DataStore (SharedPreferences like APIs) +dependencies { + implementation "androidx.datastore:datastore-preferences:1.0.0-beta01" + + // optional - RxJava2 support + implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0-beta01" + + // optional - RxJava3 support + implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0-beta01" +} +// Alternatively - use the following artifact without an Android dependency. +dependencies { + implementation "androidx.datastore:datastore-preferences-core:1.0.0-beta01" +} +``` + +**注意**:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 [Room](https://developer.android.com/training/data-storage/room),而不是DataStore。DataStore非常适合简单的小型数据集,不支持部分更新或参照完整性。 + + + + + +## 使用Preferences DataStore存储键值对 + +Preferences DataStore实现使用[DataStore](https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore)和[Preferences](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/Preferences) 类将简单的键值对保留在磁盘上。 + +### 创建Preferences DataStore + +使用由[preferencesDataStore](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/package-summary#dataStore) 创建的属性委托来创建Datastore实例。在您的Kotlin文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将DataStore保留为单例。 + +``` +// At the top level of your kotlin file +val Context.dataStore: DataStore by preferencesDataStore(name = "settings") +``` + +### 从Preferences DataStore读取内容 + +由于Preferences DataStore不使用预定义的架构,因此您必须使用相应的键类型函数为需要存储在DataStore实例中的每个值定义一个键。例如,如需为int 值定义一个键,请使用intPreferencesKey()](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary#intPreferencesKey(kotlin.String))。然后,使用[DataStore.data](https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore#data) 属性,通过Flow 提供适当的存储值。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +```kotlin +val EXAMPLE_COUNTER = intPreferencesKey("example_counter")val exampleCounterFlow: Flow = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0} +``` + +### 将内容写入Preferences DataStore + +Preferences DataStore提供了一个[edit()](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary#edit) 函数,用于以事务方式更新DataStore中的数据。该函数的transform参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 }} +``` + +## 使用Proto DataStore存储类型化的对象 + +既生Preference DataStore何生Proto DataStore,它们之间有什么区别? + +- Preference DataStore主要是为了解决SharedPreferences所带来的性能问题 +- Proto DataStore比Preference DataStore 更加灵活,支持更多的类型 + - Preference DataStore支持Int、Long、Boolean、Float、String + - protocol buffers支持的类型,Proto DataStore都支持 +- Preference DataStore以XML的形式存储key-value数据,可读性很好 +- Proto DataStore使用了二进制编码压缩,体积更小,速度比XML更快 + + + +## 序列化 + +序列化:将一个对象转换成可存储或可传输的状态,数据可能存储在本地或者在蓝牙、网络间进行传输。序列化大概分为对象序列化、数据序列化。 + +### 对象的序列化 + +**Java对象序列化** 将一个存储在内存中的对象转化为可传输的字节序列,便于在蓝牙、网络间进行传输或者存储在本地。把字节序列还原为存储在内存中的Java对象的过程称为**反序列化**。 + +在Android中可以通过Serializable和Parcelable两种方式实现对象序列化。 + +**Serializable** + +Serializable是Java原生序列化的方式,主要通过ObjectInputStream和ObjectOutputStream来实现对象序列化和反序列化,但是在整个过程中用到了大量的反射和临时变量,会频繁的触发GC,序列化的性能会非常差,但是实现方式非常简单,来看一下ObjectInputStream和ObjectOutputStream源码里有很多反射的地方。 + +``` +ObjectOutputStream.java +private void writeObject0(Object obj, boolean unshared) + throws IOException{ + ...... + Class cl = obj.getClass(); + ...... +} + +ObjectInputStream.java +void readFields() throws IOException { + ...... + ObjectStreamField[] fields = desc.getFields(false); + for (int i = 0; i < objVals.length; i++) { + objVals[i] = + readObject0(fields[numPrimFields + i].isUnshared()); + objHandles[i] = passHandle; + } + ...... +} +复制代码 +``` + +在Android中存在大量跨进程通信,由于Serializable性能差的原因,所以Android需要更加轻量且高效的对象序列化和反序列化机制,因此Parcelable出现了。 + +**Parcelable** + +Parcelable的出现解决了Android中跨进程通信性能差的问题,而且Parcelable比Serializable要快很多,因为写入和读取的时候都是采用自定义序列化存储的方式,通过writeToParcel()方法和describeContents()方法来实现,不需要使用反射来推断它,因此性能得到提升,但是使用起来比Serializable要复杂很多。 + +为了解决复杂性问题, AndroidStudio也有对应插件简化使用过程,如果是Java语言可以使用`android parcelable code generator` 插件, 如果Kotlin语言的话可以使用 @Parcelize注解,快速的实现Parcelable序列化。 + +用一张表格汇总一下Serializable和Parcelable的区别: + +![img](https:////p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e273a80bbe564bd684b3a68f39b114cf~tplv-k3u1fbpfcp-zoom-1.image) + +### 数据序列化 + +对象序列化记录了很多信息,包括Class信息、继承关系信息、变量信息等等,但是数据序列化相比于对象序列化就没有这么多沉余信息,数据序列化常用的方式有JSON、Protocol Buffers、FlatBuffers。 + +- JSON:是一种轻量级的数据交互格式,支持跨平台、跨语言,被广泛用在网络间传输,JSON的可读性很强,但是序列化和反序列化性能却是最差的,解析过程中,要产生大量的临时变量,会频繁的触发GC,为了保证可读性,并没有进行二进制压缩,当数据量很大的时候,性能上会差一点。 + +- Protocol Buffers:它是Google开源的跨语言编码协议,可以应用到C++、C#、Dart 、Go 、Java、Python等等语言,Google内部几乎所有RPC都在使用这个协议,使用了二进制编码压缩,体积更小,速度比JSON更快,但是缺点是牺牲了可读性 + + RPC指的是跨进程远程调用,即一个进程调用另外一个进程的方法。 + +- FlatBuffers:同Protocol Buffers一样是Google开源的跨平台数据序列化库,可以应用到C++、C#,Go、Java、JavaScript、PHP、Python 等等语言,空间和时间复杂度上比其他的方式都要好,在使用过程中,不需要额外的内存,几乎接近原始数据在内存中的大小,但是缺点是牺牲了可读性 + +最后我们用一张图来分析一下JSON、Protocol Buffers、FlatBuffers 它们序列化和反序列的性能,数据来源于[JSON vs Protocol Buffers vs FlatBuffers](https://codeburst.io/json-vs-protocol-buffers-vs-flatbuffers-a4247f8bda6f) + +![img](https:////p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef8b9fdbd95e463abf64bea9a2a39588~tplv-k3u1fbpfcp-zoom-1.image) + +FlatBuffers和Protocol Buffers无论是序列化还是反序列都完胜JSON,FlatBuffers 最初是Google为游戏或者其他对性能要求很高的应用开发的,接下来我们来看一下今天主角Protocol Buffer。 + +### Protocol Buffer + +Protocol Buffer(简称Protobuf) 它是Google开源的跨语言编码协议,可以应用到C++、C#、Dart、Go、Java、Python等等语言,Google内部几乎所有RPC都在使用这个协议,使用了二进制编码压缩,体积更小,速度比JSON更快。 + +> 从[Proto3.0.0 Release Note](https://github.com/protocolbuffers/protobuf/releases/tag/v3.0.0-alpha-1) 得知:protocol buffers最初开源时,它实现了Protocol Buffers语言版本2(称为 proto2), 这也是为什么版本数从v2.0.0开始,从v3.0.0开始, 引入新的语言版本(proto3),而旧的版本(proto2)继续被支持。所以到目前为止Protobuf共两个版本proto2和proto3。 + +**proto2和proto3应该学习那个版本?** + +proto3简化了proto2的语法,提高了开发的效率,因此也带来了版本不兼容的问题,因为2019年的时候才发布proto3稳定版本,所以在这之前使用Protocol Buffer的公司,大部分项目都是使用proto2的版本,从上文的源码分析部分可知,在DataStore中使用了proto2语法,所以proto2和proto3这两种语法都同时在使用。 + +对于初学者而言直接学习proto3语法就可以了,为了适应技术迭代的变化,当掌握proto3语法之后,可以顺带了解一下proto2语法以及proto3和proto2语法的区别,这样可以更好的理解其他的开源项目。 + +为了避免混淆proto3和proto2语法,在本文仅仅分析proto3语法,当我们了解完这些基本概念之后,我们开始分析如何在项目中使用Proto DataStore。 + + + + + +Proto DataStore实现使用DataStore和[协议缓冲区](https://developers.google.com/protocol-buffers)将类型化的对象保留在磁盘上。 + +### 定义架构 + +Proto DataStore 要求在 `app/src/main/proto/` 目录的 proto 文件中保存预定义的架构。此架构用于定义您在 Proto DataStore 中保存的对象的类型。如需详细了解如何定义 proto 架构,请参阅 [protobuf 语言指南](https://developers.google.com/protocol-buffers/docs/proto3)。 + +``` +syntax = "proto3"; option java_package = "com.example.application"; option java_multiple_files = true; message Settings { int32 example_counter = 1; } +``` + +**注意**:您的存储对象的类在编译时由 proto 文件中定义的 `message` 生成。请务必重新构建您的项目。 + +### 创建 Proto DataStore + +创建 Proto DataStore 来存储类型化对象涉及两个步骤: + +1. 定义一个实现 `Serializer` 的类,其中 `T` 是 proto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。 +2. 使用由 `dataStore` 创建的属性委托来创建 `DataStore` 的实例,其中 `T` 是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。`filename` 参数会告知 DataStore 使用哪个文件存储数据,而 `serializer` 参数会告知 DataStore 第 1 步中定义的序列化器类的名称。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +object SettingsSerializer : Serializer { override val defaultValue: Settings = Settings.getDefaultInstance() override suspend fun readFrom(input: InputStream): Settings { try { return Settings.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo( t: Settings, output: OutputStream) = t.writeTo(output)}val Context.settingsDataStore: DataStore by dataStore( fileName = "settings.pb", serializer = SettingsSerializer) +``` + +### 从 Proto DataStore 读取内容 + +使用 `DataStore.data` 显示所存储对象中相应属性的 `Flow`。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val exampleCounterFlow: Flow = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter } +``` + +### 将内容写入 Proto DataStore + +Proto DataStore 提供了一个 [`updateData()`](https://developer.android.com/reference/kotlin/androidx/datastore/DataStore#updatedata) 函数,用于以事务方式更新存储的对象。`updateData()` 为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() }} +``` + +## 在同步代码中使用 DataStore + +**注意**:请尽可能避免在 DataStore 数据读取时阻塞线程。阻塞界面线程可能会导致 [ANR](https://developer.android.com/topic/performance/vitals/anr) 或界面卡顿,而阻塞其他线程可能会导致[死锁](https://en.wikipedia.org/wiki/Deadlock)。 + +DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,就可能出现这种情况。 + +Kotlin 协程提供 [`runBlocking()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 `runBlocking()` 从 DataStore 同步读取数据。RxJava 在 `Flowable` 上提供阻塞方法。以下代码会阻塞发起调用的线程,直到 DataStore 返回数据: + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val exampleData = runBlocking { context.dataStore.data.first() } +``` + +对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题: + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. }} +``` + +这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 `runBlocking()` 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。 + + + + + + + + + + + +https://developer.android.com/topic/libraries/architecture/datastore#typed-datastore + +https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html + + +https://blog.csdn.net/zzw0221/article/details/109274610 + +SharedPreferences 有着许多缺陷: 看起来可以在 UI 线程安全调用的同步 API 其实并不安全、没有提示错误的机制、缺少事务 API 等等。DataStore 是 SharedPreferences 的替代方案,它解决了 Shared Preferences 的绝大部分问题。DataStore 包含使用 Kotlin 协程和 Flow 实现的完全异步 API,可以处理数据迁移、保证数据一致性,并且可以处理数据损坏。 + +https://blog.csdn.net/weixin_42324979/article/details/112650189?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control + + + +缺点: + +- 目前Jetpack Security没有支持DataStore,所以不能像SharedPreference一样支持加密 +- 不能安全的进行IPC,这点相对于SharedPreferences没有提升,有较强IPC需求的话首选MMKV +- 使用PB进行序列化时需要额外定义IDL,这会产生一定工作量 + + + + + + + +最后用一张表格来对比一下 MMKV、DataStore、SharedPreferences 的不同之处,如果发现错误,或者有其他不同之处,期待你来一起完善。 + +![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8903ca5079314ff7a03b33f60922c1bf~tplv-k3u1fbpfcp-zoom-1.image) + + + +目前为止,MMKV 功能比 DataStore 强大,毕竟 MMKV 已经经历好几个年头了, DataStore 才只是 alpha01 版本,但是 DataStore 作为 Jetpack 的成员,背靠 Google 的支持,加上全球的开发者一起来完善,从未来角度 DataStore 功能会逐渐完善。DataStore 的版本还是挺优势的,例如:类型安全检查,DataStore 编译的时候,就会告诉开发者类型不对,相比于运行时错误,编译时错误提示更好 + + + + + + + +- [上一篇:9.App Startup简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/9.App%20Startup%E7%AE%80%E4%BB%8B.md) +- [下一篇:11.Hilt简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/11.Hilt%E7%AE%80%E4%BB%8B.md) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" "b/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" new file mode 100644 index 00000000..2d3164db --- /dev/null +++ "b/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" @@ -0,0 +1,327 @@ +# 11.Hilt简介 + +## 依赖注入 + + + +## Dagger + +说到依赖注入,做Android的人都会想到一个库:Dagger。 + +说到Dagger,大家想到的都是牛逼、高端,又难学又难用。很多使用者用着用着就会掉进了自己亲手用Dagger搭建的迷宫中,越陷越深。 + +## Hilt + +所以Android团队在Jetpack中增加了Hilt,它是一个专门针对Android平台的依赖注入库。它并不是提供了依赖注入的能力,而是提供了一种依赖注入的简单实现方式。Hilt 是在 Dagger 基础上进行开发的,减少了在项目中进行手动依赖,Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,同时 Hilt 也继承了 Dagger 优点,编译时正确性、运行时性能、并且得到了 Android Studio 的支持。 + +Hilt做的优化包括: +1. 无需编写大量的Component代码 +2. Scope也会与Component自动绑定 +3. 预定义绑定,例如Application和Activity +4. 预定义的限定符,例如@ApplicationContext和@ActivityContext + + +## Koin + +Koin - a smart Kotlin injection library to keep you focused on your app, not on your tools + +[Koin](https://insert-koin.io/)是为Kotlin开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。 + + + + + +Hilt、Dagger、Koin等等都是依赖注入库,Google也在努力不断的完善依赖注入库从Dagger到Dagger2在到现在的Hilt,因为依赖注入是面向对象设计中最好的架构模式之一,使用依赖注入库有以下优点: + +- 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。 +- 在配置scopes范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。 +- 代码变得更具可读性。 +- 易于构建对象。 +- 编写低耦合代码,更容易测试。 + + + +## 添加依赖项 + +首先,将 `hilt-android-gradle-plugin` 插件添加到项目的根级 `build.gradle` 文件中: + +```groovy +buildscript { + ... + dependencies { + ... + classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' + } +} +``` + +然后,应用 Gradle 插件并在 `app/build.gradle` 文件中添加以下依赖项: + +```groovy +... +apply plugin: 'kotlin-kapt' +apply plugin: 'dagger.hilt.android.plugin' + +android { + ... +} + +dependencies { + implementation "com.google.dagger:hilt-android:2.28-alpha" + kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" +} +``` + +**注意**:同时使用Hilt和[数据绑定](https://developer.android.com/topic/libraries/data-binding)的项目需要Android Studio 4.0或更高版本。 + +Hilt使用 [Java 8 功能](https://developer.android.com/studio/write/java8-support)。如需在项目中启用Java 8,请将以下代码添加到`app/build.gradle`文件中: + +```groovy +android { + ... + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} +``` + + + + + +## Hilt常用注解的含义 + +Hilt常用注解包含@HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@InstallIn、@Provides、@EntryPoint等等。 + +### @HiltAndroidApp + +1. 所有使用Hilt的App必须包含一个使用@HiltAndroidApp注解的Application。它会替代Dagger中的AppComponent。 +2. @HiltAndroidApp注解将会触发Hilt代码的生成,作为应用程序依赖项容器的基类。 +3. 生成的Hilt组件依附于Application的生命周期,它也是App的父组件,提供其他组件访问的依赖。 +4. 在Application中设置好@HiltAndroidApp之后,就可以使用Hilt提供的组件了,组件包含Application、Activity、Fragment、View、Service、BroadcastReceiver 等等。 + +### @AndroidEntryPoint + +Hilt提供的@AndroidEntryPoint注解用于提供Android类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)。 + +- Activity:仅仅支持ComponentActivity的子类例如FragmentActivity、AppCompatActivity等等。 +- Fragment:仅仅支持继承androidx.Fragment的Fragment +- View +- Service +- BroadcastReceiver + +如果您使用@AndroidEntryPoint为某个Android类添加注释,则还必须为依赖于该类的Android类添加注释。例如,如果您为某个Fragment添加注释,则还必须为使用该Fragment的所有Activity添加注释。 + +### @Inject + +Hilt需要知道如何从相应的组件中提供必要依赖的实例。使用@Inject注解来告诉Hilt如何提供该类的实例,它常用于构造函数、非私有字段、方法中。 + +**注意:在构建时,Hilt为Android类生成Dagger组件。然后Dagger遍历您的代码并执行以下步骤:** + +- 构建并验证依赖关系,确保没有未满足的依赖关系。 +- 生成它在运行时用于创建实际对象及其依赖项的类。 + +```kotlin +@AndroidEntryPoint +class ExampleActivity : AppCompatActivity() { + + @Inject lateinit var analytics: AnalyticsAdapter + ... +} +``` + +**注意**:由Hilt注入的字段不能为私有字段。尝试使用Hilt注入私有字段会导致编译错误。 + +### @Module + +有时,类型不能通过构造函数注入。发生这种情况可能有多种原因。例如,您不能通过构造函数注入接口。此外,您也不能通过构造函数注入不归您所有的类型,如来自外部库的类。在这些情况下,您可以使用Hilt模块向Hilt提供绑定信息。 + +Hilt模块是一个带有@Module注释的类。与[Dagger 模块](https://developer.android.com/training/dependency-injection/dagger-android#dagger-modules)一样,它会告知Hilt如何提供某些类型的实例。与Dagger 模块不同的是,您必须使用@InstallIn为Hilt模块添加注释,以告知Hilt每个模块将用在或安装在哪个Android类中。 + +常用于创建依赖类的对象(例如第三方库 OkHttp、Retrofit等等),使用@Module注解的类,需要使用@InstallIn注解指定module的范围。 + +```kotlin +@Module +@InstallIn(ApplicationComponent::class) +// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。 +object NetworkModule { +} +``` + +### @InstallIn + +使用@Module注入的类,需要使用@InstallIn注解指定module的范围,例如使用 @InstallIn(ActivityComponent::class) 注解的module会绑定到activity的生命周期上。 + +Hilt提供了以下组件来绑定依赖与对应的Android类的活动范围。 + +| Hilt 提供的组件 | 对应的 Android 类的活动范围 | +| ------------------------- | ----------------------------------------- | +| ApplicationComponent | Application | +| ActivityRetainedComponent | ViewModel | +| ActivityComponent | Activity | +| FragmentComponent | Fragment | +| ViewComponent | View | +| ViewWithFragmentComponent | View annotated with @WithFragmentBindings | +| ServiceComponent | Service | + +**注意:Hilt没有为broadcast receivers提供组件,因为Hilt直接从ApplicationComponent注入broadcast receivers。** + +Hilt会根据相应的Android类生命周期自动创建和销毁生成的组件类的实例,它们的对应关系如下表格所示。 + +| Hilt 提供的组件 | 创建对应的生命周期 | 销毁对应的生命周期 | +| ------------------------- | ---------------------- | ----------------------- | +| ApplicationComponent | Application#onCreate() | Application#onDestroy() | +| ActivityRetainedComponent | Activity#onCreate() | Activity#onDestroy() | +| ActivityComponent | Activity#onCreate() | Activity#onDestroy() | +| FragmentComponent | Fragment#onAttach() | Fragment#onDestroy() | +| ViewComponent | View#super() | View destroyed | +| ViewWithFragmentComponent | View#super() | View destroyed | +| ServiceComponent | Service#onCreate() | Service#onDestroy() | + +### @Provides + +它常用于被@Module注解标记类的内部的方法,并提供依赖项对象。 + +```kotlin +@Module +@InstallIn(ApplicationComponent::class) +// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。 +object NetworkModule { + + /** + * @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。 + * @Singleton 提供单例 + */ + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .build() + } +} +``` + +### @EntryPoint + +Hilt支持最常见的Android类Application、Activity、Fragment、View、Service、BroadcastReceiver等等,但是您可能需要在Hilt不支持的类中执行依赖注入,在这种情况下可以使用@EntryPoint注解进行创建,Hilt会提供相应的依赖。 + + + +## 如何使用 Hilt 进行依赖注入 + +我们先来看一个简单的例子,注入HiltSimple并在Application中调用它的doSomething方法。 + +```kotlin +class HiltSimple @Inject constructor() { + fun doSomething() { + Log.e(TAG, "----doSomething----") + } +} + +@HiltAndroidApp +class HiltApplication : Application() { + @Inject + lateinit var mHiltSimple: HiltSimple + + override fun onCreate() { + super.onCreate() + mHiltSimple.doSomething() + } +} +``` + +Hilt需要知道如何从相应的组件中提供必要依赖的实例。使用@Inject注解来告诉Hilt如何提供该类的实例,@Inject常用于构造函数、非私有字段、方法中。 + +## Hilt vs Koin + +我们总共从以下几个方面对Hilt和Koin进行全方面的分析: + +- AndroidStudio支持Hilt在关联代码间进行导航,支持在@Inject修饰的构造器、@Binds或者@Provides修饰的方法、限定符之间进行跳转。 + +- 项目结构:完成Hilt的依赖注入需要的文件往往多于Koin。 + +- 代码行数:使用 [Statistic](https://plugins.jetbrains.com/plugin/4509-statistic) 工具来进行代码统计,反复对比了项目编译前和编译后,Hilt生成的代码多于Koin,随着项目越来越复杂,生成的代码量会越来越多。 + + | 代码行数 | Hilt | Koin | + | -------- | ------ | ------ | + | 编译之前 | 2414 | 2414 | + | 编译之后 | 149608 | 138405 | + +- 编译时间:Hilt编译时间总是大于Koin,这个结果告诉我们,如果是在一个非常大型的项目,这个代价是非常昂贵。 + + ``` + Hilt: + BUILD SUCCESSFUL in 35s + 27 actionable tasks: 27 executed + + Koin: + BUILD SUCCESSFUL in 18s + 27 actionable tasks: 27 executed + ``` + +- 使用上对比:Hilt使用起来要比Koin麻烦很多,其入门门槛高于Koin,在阅读Hilt文档的时候花了好几天时间才消化,而Koin只需要花很短的时间,依赖注入部分的代码Hilt多于Koin,在一个更大更复杂的项目中所需要的代码也更多,也越来越复杂。 + + | 依赖注入框架 | Hilt | Koin | + | ------------ | ---- | ---- | + | 代码行数 | 122 | 42 | + +**为什么Hilt编译时间总是大于Koin?** + +因为在Koin中不需要使用注解,也不需要kapt,这意味着没有额外的代码生成,所有的代码都是Kotlin原始代码,所以说 Hilt编译时间总是大于Koin,从这个角度上同时也解释了,为什么会说Koin仅使用功能解析,无额外代码生成。 + +**为什么Koin不需要用到反射?** + +因为Koin基于kotlin基础上进行开发的,使用了kotlin强大的语法糖(例如 Inline、Reified 等等)和函数式编程,来看一个简单的例子。 + +```kotlin +inline fun Module.viewModel( + qualifier: Qualifier? = null, + override: Boolean = false, + noinline definition: Definition +): BeanDefinition { + val beanDefinition = factory(qualifier, override, definition) + beanDefinition.setIsViewModel() + return beanDefinition +} +``` + +内联函数支持具体化的类型参数,使用reified修饰符来限定类型参数,可以在函数内部访问它,由于函数是内联的,所以不需要反射。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hilt_koin.png?raw=true) + +If you need **compile-time** generating library – use **Hilt** over Dagger, due to simplifications provided. Also consider the best documentation and plenty of examples from the box and in the internet. + +In case of **run-time** generation – use **Koin** over Kodein because it is simpler for using, has better support with docs and examples. + + + +# 参考 + + + +- [Why would I use Android Hilt (Dagger2) when Koin is available](https://stackoverflow.com/questions/64824349/why-would-i-use-android-hilt-dagger2-when-koin-is-available) +- [Android 中的依赖项注入](https://developer.android.com/training/dependency-injection) +- [How Dagger, Hilt and Koin differ under the hood?](https://proandroiddev.com/how-dagger-hilt-and-koin-differ-under-the-hood-c3be1a2959d7) +- [Finally, a loveable dependency injection for Android: Hilt & Koin](https://medium.com/@genc.tasbasi/finally-a-loveable-dependency-injection-for-android-hilt-koin-6cdfc08a6401) +- [Koin — Dependency Injection for Android](https://medium.com/@genc.tasbasi/koin-dependency-injection-for-android-ab521da7e6f8) +- [**Differences and Advantages of Hilt, Dagger2, Koin — 1**](https://medium.com/huawei-developers/differences-and-advantages-of-hilt-dagger2-koin-1-3af44e9a0722) +- [Comparing Three Dependency Injection Solutions](https://androidessence.com/comparing-three-dependency-injection-solutions) +- [Hilt, Koin, Kodein](https://smartbright.blog/?p=86) +- [Change DI Library from Koin to Dagger-Hilt](https://proandroiddev.com/change-di-library-from-koin-to-dagger-hilt-53a4fb3e8dd0) +- [Jetpack新成员,一篇文章带你玩转Hilt和依赖注入](https://blog.csdn.net/guolin_blog/article/details/109787732) + + + + + + + +- [上一篇:10.DataStore简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/10.DataStore%E7%AE%80%E4%BB%8B.md) +- [下一篇:12.Navigation简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/12.Navigation%E7%AE%80%E4%BB%8B.md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` diff --git "a/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" new file mode 100644 index 00000000..38c42a53 --- /dev/null +++ "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" @@ -0,0 +1,509 @@ +# 12.Navigation简介 + +单个Activity嵌套多个Fragment的UI架构模式,已经被大多数Android工程师所接受和采用。但是,对Fragment的管理一直是一件比较麻烦的事情。工程师需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。页面的切换通常还包括对应用程序App bar的管理、Fragment间的切换动画,以及Fragment间的参数传递。纯代码的方式使用起来不是特别友好,并且Fragment和App bar在管理和使用的过程中显得很混乱。 + +为此,Jetpack提供了一个名为Navigation的组件,旨在方便我们管理页面和App bar。 + + +Navigation 是一个框架,用于在 Android 应用中的“目的地”之间导航,该框架提供一致的 API,无论目的地是作为 fragment、activity 还是其他组件实现。 + + + +它具有以下优势: + +- 可视化的页面导航图,类似于Apple Xcode中的StoryBoard,便于我们理清页面间的关系。 +- 通过destination和action完成页面间的导航。 +- 方便添加页面切换动画。 +- 页面间类型安全的参数传递。 +- 通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理。 +- 支持深层链接DeepLink。 + +Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 `NavHostFragment`。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。 + + +在使用过程中,我们感受到如下的优点。 + +- 页面跳转性能更好,在单 Activity 的架构下,都是 fragment 的切换,每次 fragment 被压栈之后,View 被销毁,相比之前 Activity 跳转,更加轻量,需要的内存更少。 +- 通过 Viewmodel 进行数据共享更便捷,不需要页面之间来回传数据。 +- 统一的 Navigation API 来更精细的控制跳转逻辑。 + + + + +## 依赖 + +如果想要使用Navigation,需要现在build.gradle文件中添加以下依赖: + +``` +dependencies { + def nav_version = "2.3.5" + + // Java language implementation + implementation "androidx.navigation:navigation-fragment:$nav_version" + implementation "androidx.navigation:navigation-ui:$nav_version" + + // Kotlin + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + // Feature module Support + implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" + + // Testing Navigation + androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" + + // Jetpack Compose Integration + implementation "androidx.navigation:navigation-compose:1.0.0-alpha10" +} + +``` + + + + + +## Navigation的主要元素 + +导航组件由以下三个关键部分组成: + +1. Navigation Graph : 图表 + + 一种数据结果,用于定义应用中的所有导航目的地以及它们如何连接在一起。 + 在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为*目标*)以及用户可以通过应用获取的可能路径。 + + +2. NavHost : 主机 + + 显示导航图中目标的空白容器。导航组件包含一个默认NavHost实现 (NavHostFragment),可显示Fragment目标。 + +3. NavController : 控制器 + + 在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。 + 该控制器提供了一些方法,可用于在目的地之间导航、处理深层链接、管理返回堆栈等。 + +在应用中导航时,您告诉NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController便会在NavHost中显示相应目标。 + + + +## Navigation Graph(导航图) + +导航图是一种资源文件,其中包含您的所有目的地和操作。该图表会显示应用的所有导航路径。 + + + +如需向项目添加导航图,请执行以下操作: + +1. 在“Project”窗口中,右键点击 `res` 目录,然后依次选择 **New > Android Resource File**。此时系统会显示 **New Resource File** 对话框。 +2. 在 **File name** 字段中输入名称,例如“nav_graph”。 +3. 从 **Resource type** 下拉列表中选择 **Navigation**,然后点击 **OK**。 + +``` + + + +``` + +`` 元素是导航图的根元素。当您向图表添加目的地和连接操作时,可以看到相应的 `` 和 `` 元素在此处显示为子元素。如果您有[嵌套图表](https://developer.android.com/guide/navigation/navigation-nested-graphs),它们将显示为子 `` 元素。 + +## 向 Activity 添加 NavHost + +导航宿主是 Navigation 组件的核心部分之一。导航宿主是一个空容器,用户在您的应用中导航时,目的地会在该容器中交换进出。 + +导航宿主必须派生于 [`NavHost`](https://developer.android.com/reference/androidx/navigation/NavHost)。Navigation 组件的默认 `NavHost` 实现 ([`NavHostFragment`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment)) 负责处理 Fragment 目的地的交换。 + + + +### 通过 XML 添加 NavHostFragment + +以下 XML 示例显示了作为应用主 Activity 一部分的 `NavHostFragment`: + +```xml + + + + + + + + + + +``` + +NavHostFragment是一个特殊的Fragment,我们需要将其添加到Activity的布局文件中,作为其他Fragment的容器。 + +请注意以下几点: + +- `android:name` 属性包含 `NavHost` 实现的类名称。 +- `app:navGraph` 属性将 `NavHostFragment` 与导航图相关联。导航图会在此 `NavHostFragment` 中指定用户可以导航到的所有目的地。 +- `app:defaultNavHost="true"` 属性确保您的 `NavHostFragment` 会自动处理系统返回键,即当用户按下手机的返回按钮时,系统能自动将当前所展示的Fragment退出。请注意,只能有一个默认 `NavHost`。如果同一布局(例如,双窗格布局)中有多个宿主,请务必仅指定一个默认 `NavHost`。 + + + +```xml + + // 起始fragment + + +``` + +### Action + +```xml + + + + + + + + +``` + +在导航图中,操作由 `` 元素表示。操作至少应包含自己的 ID 和用户应转到的目的地的 ID。 + +## 导航到目的地 + +导航到目的地是使用 [`NavController`](https://developer.android.com/reference/androidx/navigation/NavController) 完成的,它是一个在 `NavHost` 中管理应用导航的对象。每个 `NavHost` 均有自己的相应 `NavController`。您可以使用以下方法之一检索 `NavController`: + +**Kotlin**: + +- [`Fragment.findNavController()`](https://developer.android.com/reference/kotlin/androidx/navigation/fragment/package-summary#findnavcontroller) +- [`View.findNavController()`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#(android.view.View).findNavController()) +- [`Activity.findNavController(viewId: Int)`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#findnavcontroller) + +**Java**: + +- [`NavHostFragment.findNavController(Fragment)`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment#findNavController(android.support.v4.app.Fragment)) +- [`Navigation.findNavController(Activity, @IdRes int viewId)`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.app.Activity, int)) +- [`Navigation.findNavController(View)`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.view.View)) + +使用 `FragmentContainerView` 创建 `NavHostFragment`,或通过 `FragmentTransaction` 手动将 `NavHostFragment` 添加到您的 Activity 时,尝试通过 `Navigation.findNavController(Activity, @IdRes int)` 检索 Activity 的 `onCreate()` 中的 `NavController` 将失败。您应改为直接从 `NavHostFragment` 检索 `NavController`。 + +```kotlin +val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment +val navController = navHostFragment.navController +navController.navigate(R.id.action_blankFragment_to_blankFragment2) +``` + +对于按钮,您还可以使用 [`Navigation`](https://developer.android.com/reference/androidx/navigation/Navigation) 类的 [`createNavigateOnClickListener()`](https://developer.android.com/reference/androidx/navigation/Navigation#createNavigateOnClickListener(int)) 便捷方法导航到目的地,如下例所示: + +```kotlin +button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null)) + +``` + + + +## 使用 DeepLinkRequest 导航 + +您可以使用 [`navigate(NavDeepLinkRequest)`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(androidx.navigation.NavDeepLinkRequest)) 直接导航到[隐式深层链接目的地](https://developer.android.com/guide/navigation/navigation-deep-link#implicit),如下例所示: + +```kotlin +val request = NavDeepLinkRequest.Builder + .fromUri("android-app://androidx.navigation.app/profile".toUri()) + .build() +findNavController().navigate(request) + +``` + +## 导航和返回堆栈 + +Android 会维护一个[返回堆栈](https://developer.android.com/guide/components/activities/tasks-and-back-stack),其中包含您之前访问过的目的地。当用户打开您的应用时,应用的第一个目的地就放置在堆栈中。每次调用 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 方法都会将另一目的地放置到堆栈的顶部。点按**向上**或**返回**会分别调用 [`NavController.navigateUp()`](https://developer.android.com/reference/androidx/navigation/NavController#navigateUp()) 和 [`NavController.popBackStack()`](https://developer.android.com/reference/androidx/navigation/NavController#popBackStack()) 方法,用于移除(或弹出)堆栈顶部的目的地。 + +`NavController.popBackStack()` 会返回一个布尔值,表明它是否已成功返回到另一个目的地。当返回 `false` 时,最常见的情况是手动弹出图的起始目的地。 + +如果该方法返回 `false`,则 `NavController.getCurrentDestination()` 会返回 `null`。您应负责导航到新目的地,或通过对 Activity 调用 `finish()` 来处理弹出情况,如下例所示: + +```kotlin + +if (!navController.popBackStack()) { + // Call finish() on your Activity + finish() +} + +``` + +## popUpTo 和 popUpToInclusive + +使用操作进行导航时,您可以选择从返回堆栈上弹出其他目的地。例如,如果您的应用具有初始登录流程,那么在用户登录后,您应将所有与登录相关的目的地从返回堆栈上弹出,这样返回按钮就不会将用户带回登录流程。 + +如需在从一个目的地导航到另一个目的地时弹出目的地,请在关联的 `` 元素中添加 `app:popUpTo` 属性。`app:popUpTo` 会告知 Navigation 库在调用 `navigate()` 的过程中从返回堆栈上弹出一些目的地。属性值是应保留在堆栈中的最新目的地的 ID。 + +您还可以添加 `app:popUpToInclusive="true"`,以表明在 `app:popUpTo` 中指定的目的地也应从返回堆栈中移除。 + +## 通过 引用其他导航图 + +在导航图中,您可以使用 `include` 引用其他图。虽然这在功能上与使用嵌套图相同,但 `include` 可让您使用其他项目模块或库项目中的图,如以下示例所示: + +```xml + + + + + + + + + + + ... + +``` + +```xml + + + + + + +``` + + + +## 创建全局操作 + +您可以使用全局操作来创建可由多个目的地共用的通用操作。例如,您可能想要不同目的地中的多个按钮导航到同一应用主屏幕。 + +```xml + + + + ... + + + + +``` + +如需在代码中使用某个全局操作,请将该全局操作的资源 ID 传递到每个界面元素的 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 方法,如以下示例所示: + +```kotlin +viewTransactionButton.setOnClickListener { view -> + view.findNavController().navigate(R.id.action_global_mainFragment) +} +``` + +## 使用 Safe Args 实现类型安全的导航 + +如需在目的地之间导航,建议使用 Safe Args Gradle 插件。此插件可生成简单的对象和构建器类,以便在目的地之间实现类型安全的导航。我们强烈建议您在导航以及[在目的地之间传递数据](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)时使用 Safe Args。 + + + +如需将 [Safe Args](https://developer.android.com/topic/libraries/architecture/navigation/navigation-pass-data#Safe-args) 添加到您的项目中,请在顶层 `build.gradle` 文件中包含以下 `classpath`: + +```xml +buildscript { + repositories { + google() + } + dependencies { + def nav_version = "2.3.5" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + } +} +``` + +您还必须应用以下两个可用插件之一。 + +如需生成适用于 Java 模块或 Java 和 Kotlin 混合模块的 Java 语言代码,请将以下行添加到**应用或模块**的 `build.gradle` 文件中: + +`apply plugin: "androidx.navigation.safeargs"` + +此外,如需生成适用于 Kotlin 独有的模块的 Kotlin 代码,请添加以下行: + +`apply plugin: "androidx.navigation.safeargs.kotlin"` + +根据[迁移到 AndroidX](https://developer.android.com/jetpack/androidx/migrate#migrate)) 文档,您的 [`gradle.properties` 文件](https://developer.android.com/studio/build#properties-files)中必须具有 `android.useAndroidX=true`。 + +启用 Safe Args 后,生成的代码会包含已定义的每个操作的类和方法,以及与每个发送目的地和接收目的地相对应的类。 + +Safe Args 为生成操作的每个目的地生成一个类。生成的类名称会在源目的地类名称的基础上添加“Directions”。例如,如果源目的地的名称为 `SpecifyAmountFragment`,则生成的类的名称为 `SpecifyAmountFragmentDirections`。 + +生成的类为源目的地中定义的每个操作提供了一个静态方法。该方法接受任何定义的[操作参数](https://developer.android.com/guide/navigation/navigation-pass-data)为参数,并返回可直接传递到 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController.html?skip_cache=true#navigate(androidx.navigation.NavDirections)) 的 [`NavDirections`](https://developer.android.com/reference/androidx/navigation/NavDirections.html?skip_cache=true) 对象。 + + + +### Safe Args 示例 + +例如,假设我们的导航图包含一个操作,该操作将两个目的地 `SpecifyAmountFragment` 和 `ConfirmationFragment` 连接起来。`ConfirmationFragment` 接受您作为操作的一部分提供的单个 `float` 参数。 + +Safe Args 会生成一个 `SpecifyAmountFragmentDirections` 类,其中只包含一个 `actionSpecifyAmountFragmentToConfirmationFragment()` 方法和一个名为 `ActionSpecifyAmountFragmentToConfirmationFragment` 的内部类。这个内部类派生自 `NavDirections` 并存储了关联的操作 ID 和 `float` 参数。然后,您可以将返回的 `NavDirections` 对象直接传递到 `navigate()`,如下例所示: + +```kotlin +override fun onClick(v: View) { + val amount: Float = ... + val action = + SpecifyAmountFragmentDirections + .actionSpecifyAmountFragmentToConfirmationFragment(amount) + v.findNavController().navigate(action) +} +``` + +## 传递参数 + +Navigation 支持您通过定义目的地参数将数据附加到导航操作。例如,用户个人资料目的地可能会根据用户 ID 参数来确定要显示哪个用户。 + +通常情况下,强烈建议您仅在目的地之间传递最少量的数据。例如,您应该传递键来检索对象而不是传递对象本身,因为在 Android 上用于保存所有状态的总空间是有限的。如果您需要传递大量数据,不妨考虑使用 [`ViewModel`](https://developer.android.com/reference/androidx/lifecycle/ViewModel)(如[在 Fragment 之间共享数据](https://developer.android.com/topic/libraries/architecture/viewmodel#sharing)中所述)。 + +```xml + + + +``` + +通过声明argement节点来指定参数。 + +启用 Safe Args 后,生成的代码会为每个操作包含以下类型安全的类和方法,以及每个发送和接收目的地。 + +- 为生成操作的每一个目的地创建一个类。该类的名称是在源目的地的名称后面加上“Directions”。例如,如果源目的地是名为 `SpecifyAmountFragment` 的 Fragment,则生成的类的名称为 `SpecifyAmountFragmentDirections`。 + + 该类会为源目的地中定义的每个操作提供一个方法。 + +- 对于用于传递参数的每个操作,都会创建一个 inner 类,该类的名称根据操作的名称确定。例如,如果操作名称为 `confirmationAction,`,则类名称为 `ConfirmationAction`。如果您的操作包含不带 `defaultValue` 的参数,则您可以使用关联的 action 类来设置参数值。 + +- 为接收目的地创建一个类。该类的名称是在目的地的名称后面加上“Args”。例如,如果目的地 Fragment 的名称为 `ConfirmationFragment,`,则生成的类的名称为 `ConfirmationFragmentArgs`。可以使用该类的 `fromBundle()` 方法检索参数。 + +``` +override fun onClick(v: View) { val amountTv: EditText = view!!.findViewById(R.id.editTextAmount) val amount = amountTv.text.toString().toInt() val action = SpecifyAmountFragmentDirections.confirmationAction(amount) v.findNavController().navigate(action)} +``` + +在接收目的地的代码中,请使用 [`getArguments()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getArguments()) 方法来检索 bundle 并使用其内容。使用 `-ktx` 依赖项时,Kotlin 用户还可以使用 `by navArgs()` 属性委托来访问参数。 + +```kotlin +val args: ConfirmationFragmentArgs by navArgs() + +override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val tv: TextView = view.findViewById(R.id.textViewAmount) + val amount = args.amount + tv.text = amount.toString() +} +``` + +## 使用 Bundle 对象在目的地之间传递参数 + +如果您不使用 Gradle,仍然可以使用 `Bundle` 对象在目的地之间传递参数。创建 `Bundle` 对象并使用 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 将它传递给目的地,如下所示: + +``` +val bundle = bundleOf("amount" to amount)view.findNavController().navigate(R.id.confirmationAction, bundle) +``` + +在接收目的地的代码中,请使用 [`getArguments()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getArguments()) 方法来检索 `Bundle` 并使用其内容: + +```kotlin +val tv = view.findViewById(R.id.textViewAmount) +tv.text = arguments?.getString("amount") +``` + + + +## NavigationUI + +导航图是Navigation组件中很重要的一部分,它可以帮助我们快速了解页面之间的关系,再通过NavController便可以完成页面的切换工作。而在页面的切换过程中,通常还伴随着App bar中menu菜单的变化。对于不同的页面,App bar中的menu菜单很可能是不一样的。App bar中的各种按钮和菜单,同样承担着页面切换的工作。例如,当ActionBar左边的返回按钮被单击时,我们需要响应该事件,返回到上一个页面。既然Navigation和App bar都需要处理页面切换事件,那么,为了方便管理,Jetpack引入了NavigationUI组件,使App bar中的按钮和菜单能够与导航图中的页面关联起来。 + +`NavigationUI` 支持以下顶部应用栏类型: + +- [`Toolbar`](https://developer.android.com/reference/android/widget/Toolbar) +- [`CollapsingToolbarLayout`](https://developer.android.com/reference/com/google/android/material/appbar/CollapsingToolbarLayout) +- [`ActionBar`](https://developer.android.com/reference/androidx/appcompat/app/ActionBar) + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + ... + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navController = navHostFragment.navController + val appBarConfiguration = AppBarConfiguration( + topLevelDestinationIds = setOf(), + fallbackOnNavigateUpListener = ::onSupportNavigateUp + ) + findViewById(R.id.toolbar) + .setupWithNavController(navController, appBarConfiguration) +} + +``` + +### 参考 + +https://mp.weixin.qq.com/s?src=11×tamp=1712714064&ver=5191&signature=JTMgHGLtMGW*NoSWSrLNVuGzs-KEEDznO-ja7*X*KumZMFAuIRl7WbPYT1gG7AX810nUx6Ftb6nm6Ao92M*GzojPfqBUo1wOFc0gMs1mseTLkUWZ9Q*BIW69MM7ULPDV&new=1 + + +- [上一篇:11.Hilt简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/11.Hilt%E7%AE%80%E4%BB%8B.md) +- [下一篇:13.Jetpack MVVM简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/13.Jetpack%20MVVM%E7%AE%80%E4%BB%8B.md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Jetpack/architecture/13.Jetpack MVVM\347\256\200\344\273\213.md" "b/Jetpack/architecture/13.Jetpack MVVM\347\256\200\344\273\213.md" new file mode 100644 index 00000000..50a25fa1 --- /dev/null +++ "b/Jetpack/architecture/13.Jetpack MVVM\347\256\200\344\273\213.md" @@ -0,0 +1,50 @@ +# 13.Jetpack MVVM简介 + +项目地址:[android-architecture](https://github.com/googlesamples/android-architecture) +`Google`将该项目命名为`Android`的架构蓝图,我想从名字上已可以看穿一切。 + +在它的官方介绍中是这样说的: + +> The Android framework offers a lot of flexibility when it comes to defining how to organize and architect an Android app. This freedom, whilst very valuable, can also result in apps with large classes, inconsistent naming and architectures (or lack of) that can make testing, maintaining and extending difficult. + +> Android Architecture Blueprints is meant to demonstrate possible ways to help with these common problems. In this project we offer the same application implemented using different architectural concepts and tools. + +> You can use these samples as a reference or as a starting point for creating your own apps. The focus here is on code structure, architecture, testing and maintainability. However, bear in mind that there are many ways to build apps with these architectures and tools, depending on your priorities, so these shouldn't be considered canonical examples. The UI is deliberately kept simple. + +Jetpack MVVM 是 MVVM 模式在 Android 开发中的一个具体实现,是 Android中 Google 官方提供并推荐的 MVVM实现方式。 +不仅通过数据驱动完成彻底解耦,还兼顾了 Android 页面开发中其他不可预期的错误,例如Lifecycle 能在妥善处理 页面生命周期 避免view空指针问题,ViewModel使得UI发生重建时 无需重新向后台请求数据,节省了开销,让视图重建时更快展示数据。 +首先,请查看下图,该图显示了所有模块应如何彼此交互: + +各模块对应MVVM架构: + +View层:Activity/Fragment +ViewModel层:Jetpack ViewModel + Jetpack LivaData +Model层:Repository仓库,包含 本地持久性数据 和 服务端数据 + +View层 包含了我们平时写的Activity/Fragment/布局文件等与界面相关的东西。 +ViewModel层 用于持有和UI元素相关的数据,以保证这些数据在屏幕旋转时不会丢失,并且还要提供接口给View层调用以及和仓库层进行通信。 +仓库层 要做的主要工作是判断调用方请求的数据应该是从本地数据源中获取还是从网络数据源中获取,并将获取到的数据返回给调用方。本地数据源可以使用数据库、SharedPreferences等持久化技术来实现,而网络数据源则通常使用Retrofit访问服务器提供的Webservice接口来实现。 +另外,图中所有的箭头都是单向的,例如View层指向了ViewModel层,表示View层会持有ViewModel层的引用,但是反过来ViewModel层却不能持有View层的引用。除此之外,引用也不能跨层持有,比如View层不能持有仓库层的引用,谨记每一层的组件都只能与它相邻层的组件进行交互。 +这种设计打造了一致且愉快的用户体验。无论用户上次使用应用是在几分钟前还是几天之前,现在回到应用时都会立即看到应用在本地保留的数据。如果此数据已过期,则应用的Repository将开始在后台更新数据。 + +有人可能会有疑惑:怎么完全没有提 DataBinding、双向绑定? +实际上,这也是我之前的疑惑。 没有提 是因为: + +我不想让读者 一提到 MVVM 就和DataBinding联系起来 +我想让读者 抓住 MVVM 数据驱动 的本质。 +而DataBinding提供的双向绑定,是用来完善Jetpack MVVM 的工具,其本身在业界又非常具有争议性。 +掌握本篇内容,已经是Google推荐的开发架构,就已经实现 MVVM 模式。在Google官方的 应用架构指南 中 也同样丝毫没有提到 DataBinding。 + + +## 参考 +- [“终于懂了“系列:Jetpack AAC完整解析(四)MVVM - Android架构探索!](https://juejin.cn/post/6921321173661777933) + + +- [上一篇:12.Navigation简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/12.Navigation%E7%AE%80%E4%BB%8B.md) +- [下一篇:14.findViewById的过去及未来](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/14.findViewById%E7%9A%84%E8%BF%87%E5%8E%BB%E5%8F%8A%E6%9C%AA%E6%9D%A5.md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` \ No newline at end of file diff --git "a/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" "b/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" new file mode 100644 index 00000000..fe29ca19 --- /dev/null +++ "b/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" @@ -0,0 +1,28 @@ +# 14.findViewById的过去及未来 + +We have lots of alternatives for this, and you may wonder why do we need another solution. Let’s compare the different solutions based on these criteria: null-safety, compile-time safety, and speed. + +| Column 1 | **[ButterKnife](https://github.com/JakeWharton/butterknife)** | [**Kotlin Synthetics**](https://developer.android.com/kotlin/ktx) | [**Data Binding**](https://developer.android.com/topic/libraries/data-binding) | [**findViewById**](https://developer.android.com/reference/android/app/Activity#findViewById(int)) | [View Binding](https://developer.android.com/topic/libraries/view-binding) | +| --------------------- | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| **Fast** | ❌ * | ✅ | ❌ * | ✅ | ✅ | +| **Null-safe** | ❌ | ❌ | ✅ | ❌ | ✅ | +| **Compile-time safe** | ❌ | ❌ | ✅ | ✅ ** | ✅ | + +\* ButterKnife and Data Binding solutions are slower because they use an annotation-based approach + ** `findViewById()` is compile-time safe since API 26 because we don’t need to cast the type of view anymore. + +https://juejin.cn/post/6905942568467759111 + +https://medium.com/mobile-app-development-publication/how-android-access-view-item-the-past-to-the-future-bb003ae84527 + + + +## 参考 +- [Kotlin 插件的落幕,ViewBinding 的崛起](https://juejin.cn/post/6905942568467759111) +- [How Android Access View Item: The Past to the Future](https://medium.com/mobile-app-development-publication/how-android-access-view-item-the-past-to-the-future-bb003ae84527) + + + +- [上一篇:13.Jetpack MVVM简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/13.Jetpack%20MVVM%E7%AE%80%E4%BB%8B.md) + + diff --git "a/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" new file mode 100644 index 00000000..4718b3f9 --- /dev/null +++ "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" @@ -0,0 +1,426 @@ +# 2.ViewBinding简介 + +ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工具,在Android Studio 3.6中添加的一个新功能,更准确的说,它是DataBinding的一个更轻量变体,为什么要使用View Binding呢?答案是性能。许多开发者使用Data Binding库来引用Layout XML中的视图,而忽略它的其他强大功能。相比来说,自动生成代码ViewBinding其实比DataBinding性能更好。但是传统的方式使用View Binding却不是很好,因为会有很多样板代码(垃圾代码)。 + +通过ViewBinding,你可以更轻松的编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个XML布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有ID的所有视图的直接引用。在大多数情况下,视图绑定会替代findViewById。 + +## 使用方法 + +### 1.build.gradle中开启 +在build.gradle文件中的android节点添加如下代码: +``` +android { + ... + buildFeatures { + viewBinding true + } +} +``` +重新编译后系统会为每个布局文件生成对应的Binding类,该类中包含对应布局中具有id的所有视图的直接饮用。生成类的目录在app/build/generated/data_binding_base_class_source_out中。 +如果项目中存在多个模块,则需要在每个模块的build.gradle文件中都加上该配置。 +假设某个布局文件的名称为result_profile.xml: + +```xml + + + +