论坛首页 Java企业应用论坛

利用反射进行深层克隆

浏览 10146 次
精华帖 (0) :: 良好帖 (1) :: 新手帖 (2) :: 隐藏帖 (0)
作者 正文
   发表时间:2010-05-05   最后修改:2010-05-06

最近在看《effective java》,其中有一节谈到了克隆,所以今天想来就来研究一下克隆。

 

我们大家都知道,对一个对应进行复制有二种比较好的方式,一种就是序列化,另一种就是克隆。使用序列化进行复制很方便,因为此种方式会自动进行深层复制,只需要我们将要序列化的对象所对应的类实现序列化标示性接口Serializable,它就会将对象里所引用的其他对象一并复制,但此种效率不及Object里的clone克隆方法。不过使用clone进行克隆却是浅复制,它不会自动将对象里所引用的其他对象进行深层克隆,所以如果我们想要进行深层复制时,需要覆写Object中的clone方法,对需要进行深层复制的域进行单独处理,所以应用起来比较麻烦,正是因为这样繁琐,下面我采用了反射的方式来进行深层克隆clone,其只需要克隆的类继承该类DeepClone即可。详细过程请参见注释。

 

深层克隆实现:

/**
 * 
 * 利用反射进行深度克隆,只要继承该类的Bean就具有深度克隆的能力。
 * 
 * 但是不支持克隆父类的属性成员,因为 this.getClass().getDeclaredFields()
 * 只能获取到自己本身所有定义的属性成员,所以此继承的情况下不支持父类的属性成员深
 * 度克隆,除非放弃这种反射,为每个Bean覆写clone方法。
 * 
 * 另外需注意的是,本程序只是对实现了Cloneable接口并重写了clone方法的类实例才进行
 * 深层克隆,如果你的类里含有未实现Cloneable接口的引用类型,则不会帮你进行深层克隆
 * (虽然可以做,比如使用序列化与反序列化来创建另一个实例,但这么做违背了这个类最
 * 初的设计 —— 它本身就是一个不可变类或都是一个不具有状态的如工具类,则创建多个这样
 * 的实例没有什么好处,反而会占用内存与频繁的调用垃圾回器;如果这个类是可变的而没有
 * 实现克隆接口,那么这则是设计人员本身的的设计错误,所以这里不会帮你去克隆这些类)。
 * 
 * 请记住,克隆对那些可变的值类类型的Bean才具有实际意义,对不可变类或者是不具有状态
 * 的类对象克隆没有意义,Java库里的不可变值类型类就是这么处理的,比如String、基本
 * 类型的包装类、BigInteger...,它们都不具有克隆能力
 * 
 * @author jiangzhengjun 2010.5.5
 */
public abstract class DeepClone implements Cloneable, Serializable {

	protected Object clone() throws CloneNotSupportedException {
		Object cloneObj = null;
		try {
			// 克隆对象
			cloneObj = super.clone();

			// 该类的所有属性,包括静态属性
			Field[] filedArr = this.getClass().getDeclaredFields();
			Field field;//属性
			Class fieldType;//属性类型
			Object filedVal;//属性值
			for (int i = 0; i < filedArr.length; i++) {
				field = filedArr[i];
				fieldType = field.getType();
				field.setAccessible(true);
				filedVal = field.get(this);
				/*
				下面代码运行的结果可以表明super.clone()只是浅复制,它只是将原始对象
				的域成员内存地址对拷到了克隆对象中,所以如果是引用类型则指向同一对象,
				若是基本类型,则直接将存储的值复制到克隆对象中,基本类型域成员不需要
				再次单独复制处理。然而,引用类型却是线复制,所以我们需要对引用型单独
				做特殊的复制处理,即深层克隆。
				
				下面是某次的输出结果,从输出结果可以证实上面的结论:					
				i : -1 - -1
				ca : CloneA@480457 - CloneA@480457
				ca1 : CloneA@47858e - CloneA@47858e
				ca2 : CloneA@19134f4 - CloneA@19134f4
				cb : CloneB@df6ccd - CloneB@df6ccd
				sb :  - 
				intArr : [[[I@601bb1 - [[[I@601bb1
				caArr : [[[LCloneA;@1ea2dfe - [[[LCloneA;@1ea2dfe
				cbArr : [[[LCloneB;@17182c1 - [[[LCloneB;@17182c1
				int1Arr : [I@13f5d07 - [I@13f5d07
				ca1Arr : [LCloneA;@f4a24a - [LCloneA;@f4a24a
				cb1Arr : [LCloneB;@cac268 - [LCloneB;@cac268
				*/
				//Field clFiled = cloneObj.getClass().getDeclaredField(
				//		field.getName());
				//clFiled.setAccessible(true);
				//System.out.println(field.getName() + " : " + filedVal + " - "
				//		+ clFiled.get(cloneObj));
				/*
				 * 如果是静态的成员,则不需要深层克隆,因为静态成员属于类成员,
				 * 对所有实例都共享,不要改变现有静态成员的引用指向。
				 * 
				 * 如果是final类型变量,则不能深层克隆,即使复制一份后也不能将
				 * 它赋值给final类型变量,这也正是final的限制。否则在使用反射
				 * 赋值给final变量时会抛异常。所以在我们定义一个引用类型是否是
				 * final时,我们要考虑它是否是真真不需要修改它的指向与指向内 容。
				 */
				if (Modifier.isStatic(field.getModifiers())
						|| Modifier.isFinal(field.getModifiers())) {
					continue;
				}

				//如果是数组
				if (fieldType.isArray()) {
					/*
					 * 克隆数组,但只是克隆第一维,比如是三维,克隆的结果就相当于
					 * new Array[3][][], 即只初始化第一维,第二与第三维 还需进一步
					 * 初始化。如果某个Class对象是数 组类对象,则 class.getComponen
					 * tType返回的是复合类型,即元素的类 型,如 果数组是多维的,那么
					 * 它返回的也是数组类型,类型 比class少一维而已,比如 有
					 * Array[][][] arr = new Array[3][][],则arr.getClass().getC
					 * omponentType返回的为二维Array类型的数组,而且我们可以以这个
					 * 返回的类型来动态创建 三维数组
					 */
					Object cloneArr = Array.newInstance(filedVal.getClass()
							.getComponentType(), Array.getLength(filedVal));

					cloneArr(filedVal, cloneArr);

					// 设置到克隆对象中
					filedArr[i].set(cloneObj, cloneArr);
				} else {// 如果不是数组
					/*
					 * 如果为基本类型或没有实现Cloneable的引用类型时,我们不需要对
					 * 它们做任何克隆处理,因为上面的super.clone()已经对它们进行了
					 * 简单的值拷贝工作了,即已将基本类型的值或引用的地址拷贝到克隆
					 * 对象中去了。super.clone()对基本类型还是属于深克隆,而对引用
					 * 则属于浅克隆。
					 * 
					 * String、 Integer...之类为不可变的类,它们都没有实现Cloneable,
					 * 所以对它们进行浅克隆是没有问题的,即它们指向同一不可变对象是没
					 * 有问题。对不可变对象进行克隆是没有意义的。但要 注意,如果是自己
					 * 设计的类,就要考虑是否实现Cloneable与重 写clone方法,如果没有
					 * 这样作,也会进行浅克隆。
					 * 
					 * 下面只需对实现了Cloneable的引用进行深度克隆。
					 */

					// 如果属性对象实现了Cloneable
					if (filedVal instanceof Cloneable) {

						// 反射查找clone方法
						Method cloneMethod;
						try {
							cloneMethod = filedVal.getClass().getDeclaredMethod("clone",
									new Class[] {});

						} catch (NoSuchMethodException e) {
							cloneMethod = filedVal.getClass().getMethod("clone",
									new Class[] {});
						}
						//调用克隆方法并设置到克隆对象中
						filedArr[i].set(cloneObj, cloneMethod.invoke(filedVal,
								new Object[0]));
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		return cloneObj;
	}

	/**
	 * 多维数组深层克隆,如果数组类型是实现了Cloneable接口的某个类,
	 * 则会调用每个元素的clone方法实现深度克隆
	 * 
	 * 虽然数组有clone方法,但我们不能使用反射来克隆数组,因为不能使用
	 * 反射来获取数组的clone方法,这个方法只能通过数组对象本身来调用,
	 * 所以这里使用了动态数组创建方法来实现。
	 * 
	 * @param objArr
	 * @param cloneArr
	 * @throws Exception
	 */
	static private void cloneArr(Object objArr, Object cloneArr) throws Exception {
		Object objTmp;
		Object val = null;
		for (int i = 0; i < Array.getLength(objArr); i++) {
			//注,如果是非数组的基本类型,则返回的是包装类型
			objTmp = Array.get(objArr, i);

			if (objTmp == null) {
				val = null;
			} else if (objTmp.getClass().isArray()) {//如果是数组

				val = Array.newInstance(objTmp.getClass().getComponentType(), Array
						.getLength(objTmp));
				//如果元素是数组,则递归调用
				cloneArr(objTmp, val);
			} else {//否则非数组

				/*
				 * 如果为基本类型或者是非Cloneable类型的引用类型,则直接对拷值 或
				 * 者是对象的地址。没有实现Cloneable的引用类型会实行浅复制, 这对
				 * 于像String不可变类来说是没有关系的,因为它们可以多实例或 多线程
				 * 共享,但如果即没有实现Cloneable,又是可变以的类,浅复制 则会带来
				 * 危险,因为这些类实例不能共享 ,一个实例里的改变会影响到 另一个实
				 * 例。所以在使用克隆方案的时候一定要考虑可变对象的可克隆性,即需要
				 * 实现Cloneable。
				 * 
				 * 注,这里不能使用 objTmp.getClass.isPrimitive()来判断是元素是
				 * 否是基本类型,因为objTmp是通过Array.get获得的,而Array.get返
				 * 回的是Object 类型,也就是说如果是基本类型会自动转换成对应的包
				 * 装类型后返回,所以 我们只能采用原始的类型来判断才行。
				 */
				if (objArr.getClass().getComponentType().isPrimitive()
						|| !(objTmp instanceof Cloneable)) {//基本类型或非Cloneable引用类型
					val = objTmp;
				} else if (objTmp instanceof Cloneable) {//引用类型,并实现了Cloneable
					/*
					 *  用反射查找colone方法,注,先使用getDeclaredMethod获取自
					 *  己类 中所定义的方法(包括该类所声明的公共、保护、默认访问
					 *  及私有的 方法),如果没有的话,再使用getMethod,getMethod
					 *  只能获取公有的方法,但还包括了从父类继承过来的公有方法
					 */
					Method cloneMethod;
					try {
						//先获取自己定义的clone方法
						cloneMethod = objTmp.getClass().getDeclaredMethod("clone",
								new Class[] {});

					} catch (NoSuchMethodException e) {
						//如果自身未定义clone方法,则从父类中找,但父类的clone一定要是public
						cloneMethod = objTmp.getClass()
								.getMethod("clone", new Class[] {});
					}
					cloneMethod.setAccessible(true);
					val = cloneMethod.invoke(objTmp, new Object[0]);

				}
			}
			// 设置克隆数组元素值
			Array.set(cloneArr, i, val);
		}
	}
}

 

深层克隆测试:

//具有克隆能力的测试类
class CloneA implements Cloneable, Serializable {
	int intArr[] = new int[] { 1 };

	protected Object clone() throws CloneNotSupportedException {
		CloneA clone = (CloneA) super.clone();
		clone.intArr = (int[]) intArr.clone();
		return clone;
	}
}

/*
 * 不具有克隆能力的测试类,但该类里有一个引用类型intArr,如果共享则
 * 会有问题,所以该类在克隆的方案里使用(即用在了ValueBean可克隆中)
 * 就是一个错误,这是设计人员自身的错误,问题由设计人员自已负责。
 */
class UnCloneB implements  Serializable{
	int intArr[] = new int[] { 1 };
}

class ParentBean extends DeepClone {
	/*
	 * 使用 new ValueBean().clone()时,
	 * ValueBean的父类ParentBena的属性成员不具有深度克隆的
	 * 能力,但你又不能重写DeepClone父类的clone方法,否则
	 * 反射深层克隆不再起 作用。不知道这个问题能否很好的解
	 * 决。 想了一下,除非不继承自DeepClone,在子类ValueBean
	 * 中重写Object的clone方法,然后在子类中针对该属性做单
	 * 独的克隆处理才可以。
	 */
	public final CloneA pca = new CloneA();
}

/**
 *  用来进行克隆的值Bean
 *  
 *  能克隆的域会进行深层克隆,不能克隆的域会进行浅复制
 */
class ValueBean extends ParentBean {
	private int i = -1;
	private String str = new String("string");
	public static CloneA ca = new CloneA();
	private final CloneA ca1 = new CloneA();
	private CloneA ca2 = new CloneA();
	private UnCloneB cb = new UnCloneB();
	private StringBuffer sb = new StringBuffer();

	//三维数组
	private int[][][] intArr;//基本类型数组
	private CloneA[][][] caArr;//元素实现了Cloneable的数组
	private UnCloneB[][][] cbArr;//元素未实现了Cloneable的数组

	//一维数组
	int[] int1Arr;
	CloneA[] ca1Arr;
	UnCloneB[] cb1Arr;

	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

	public ValueBean() {
		intArr = new int[3][][];
		intArr[0] = new int[2][];
		intArr[0][1] = new int[2];
		intArr[0][1][0] = 1;
		intArr[1] = new int[1][];
		intArr[1][0] = new int[2];
		intArr[1][0][0] = 2;
		intArr[1][0][1] = 3;

		caArr = new CloneA[3][][];
		caArr[0] = new CloneA[2][];
		caArr[0][1] = new CloneA[2];
		caArr[0][1][0] = new CloneA();
		caArr[1] = new CloneA[1][];
		caArr[1][0] = new CloneA[2];
		caArr[1][0][0] = new CloneA();
		caArr[1][0][1] = new CloneA();

		cbArr = new UnCloneB[3][][];
		cbArr[0] = new UnCloneB[2][];
		cbArr[0][1] = new UnCloneB[2];
		cbArr[0][1][0] = new UnCloneB();
		cbArr[1] = new UnCloneB[1][];
		cbArr[1][0] = new UnCloneB[2];
		cbArr[1][0][0] = new UnCloneB();
		cbArr[1][0][1] = new UnCloneB();

		int1Arr = new int[2];
		int1Arr[0] = 1;
		int1Arr[1] = 2;

		ca1Arr = new CloneA[3];
		ca1Arr[0] = new CloneA();
		ca1Arr[1] = new CloneA();

		cb1Arr = new UnCloneB[3];
		cb1Arr[0] = new UnCloneB();
		cb1Arr[2] = new UnCloneB();
	}

	public static void main(String[] args) throws Exception {
		ValueBean bt = new ValueBean();
		//因为是静态属性,防止克隆过程中修改,所以先存储起来,供后面对比
		CloneA ca = ValueBean.ca;
		ValueBean btclone = (ValueBean) bt.clone();

		bt.i = 10;
		System.out.println(btclone.i);//-1 ,基本类型克隆成功

		System.out.println(bt.str == btclone.str);//true,String为不可变类,没有克隆

		System.out.println(ca == ValueBean.ca);//true,静态成员没有克隆

		System.out.println(//true,final类型的引用没有深层复制
				bt.ca1 == btclone.ca1);

		System.out.println(//false,可克隆的引用类型已进行深层克隆
				bt.ca2 == btclone.ca2);

		bt.ca2.intArr[0] = 2;//试着改变可克隆原始对象的值
		System.out.println(btclone.ca2.intArr[0]);//1,CloneA里的数组克隆成功

		System.out.println(//true,不可克隆的引用类型还是浅复制
				bt.cb == btclone.cb);

		bt.cb.intArr[0] = 2;//试着改变不可克隆原始对象的值
		System.out.println(btclone.cb.intArr[0]);//2,CloneB里的数组没有深层克隆

		bt.sb.append(1);
		System.out.println(//1,不可克隆引用只进行浅复制,所以指向原始对象
				btclone.sb);

		bt.intArr[0][1][0] = 11;
		bt.intArr[1][0][0] = 22;
		bt.intArr[1][0][1] = 33;
		System.out.println(//11 1,基本类型数组克隆成功
				bt.intArr[0][1][0] + " " + btclone.intArr[0][1][0]);
		System.out.println(//22 2
				bt.intArr[1][0][0] + " " + btclone.intArr[1][0][0]);
		System.out.println(//33 3
				bt.intArr[1][0][1] + " " + btclone.intArr[1][0][1]);
		System.out.println(//null null
				bt.intArr[2] + " " + btclone.intArr[2]);

		//Cloneable引用类型数组克隆成功
		System.out.println(//CloneA@3e25a5 CloneA@19821f
				bt.caArr[0][1][0] + " " + btclone.caArr[0][1][0]);
		System.out.println(//CloneA@addbf1 CloneA@42e816
				bt.caArr[1][0][0] + " " + btclone.caArr[1][0][0]);
		System.out.println(//CloneA@9304b1 CloneA@190d11
				bt.caArr[1][0][1] + " " + btclone.caArr[1][0][1]);
		System.out.println(//null null
				bt.caArr[2] + " " + btclone.caArr[2]);

		bt.caArr[0][1][0].intArr[0] = 2;
		System.out.println(//1,即使原始对象改变了,但这里为深层克隆,所以没影响
				btclone.caArr[0][1][0].intArr[0]);

		// 对象数组本身已克隆,好比直接调用数组的 clone方法。
		System.out.println(//[[[LCloneB;@de6ced [[[LCloneB;@c17164
				bt.cbArr + " " + btclone.cbArr);
		//非Cloneable引用类型数组里克隆后里面的元素指向相同元素
		System.out.println(//CloneB@de6ced CloneB@de6ced
				bt.cbArr[0][1][0] + " " + btclone.cbArr[0][1][0]);
		System.out.println(//CloneB@c17164 CloneB@c17164
				bt.cbArr[1][0][0] + " " + btclone.cbArr[1][0][0]);
		System.out.println(//CloneB@1fb8ee3 CloneB@1fb8ee3
				bt.cbArr[1][0][1] + " " + btclone.cbArr[1][0][1]);
		System.out.println(//null null
				bt.cbArr[2] + " " + btclone.cbArr[2]);

		bt.cbArr[0][1][0].intArr[0] = 2;
		System.out.println(//2,原始对象改变影响到另一实例,因为UnCloneB不具克隆能力
				btclone.cbArr[0][1][0].intArr[0]);

		//一维数组克隆也是没有问题的
		bt.int1Arr[0] = 11;
		bt.int1Arr[1] = 22;
		System.out.println(//11 1
				bt.int1Arr[0] + " " + btclone.int1Arr[0]);
		System.out.println(//22 2
				bt.int1Arr[1] + " " + btclone.int1Arr[1]);

		System.out.println(//CloneA@ca0b6 CloneA@10b30a7
				bt.ca1Arr[0] + " " + btclone.ca1Arr[0]);
		System.out.println(//CloneA@1a758cb CloneA@1b67f74
				bt.ca1Arr[1] + " " + btclone.ca1Arr[1]);

		System.out.println(//CloneB@69b332 CloneB@69b332
				bt.cb1Arr[0] + " " + btclone.cb1Arr[0]);
		System.out.println(//null null
				bt.cb1Arr[1] + " " + btclone.cb1Arr[1]);

		//父类的属性成员没有被深度克隆
		System.out.println(bt.pca == btclone.pca);//true

		//如果直接创建父类的实例然后对它进行克隆,这与直接创建子类一样也是可以的
		ParentBean pb1 = new ParentBean();
		ParentBean pb2 = (ParentBean) pb1.clone();
		System.out.println(pb1.pca == pb2.pca);//false
	}
}

 

先就写到这里吧,以后有时间来使用序列化来实现深层克隆。

   发表时间:2010-05-06  
有点像在看jquery的原码。。。。。
0 请登录后投票
   发表时间:2010-05-06  
有点意思

不过,我认为需要对String进行区别对待.
还是就是,搞不懂,为啥一定要继承你的类呢?这样通用性很差啊...
0 请登录后投票
   发表时间:2010-05-06  
wendal 写道
有点意思

不过,我认为需要对String进行区别对待.
还是就是,搞不懂,为啥一定要继承你的类呢?这样通用性很差啊...


String为什么要区别对待?String本身就是不可变对象 ,这里根就没有对它进克隆,因为String没有实现Cloneable接口,程序不会对它再次克隆 。

不继承也可以啊,你总要有个地方来写入这些代码吧。
0 请登录后投票
   发表时间:2010-05-06  
junJZ_2008 写道
我们大家都知道,对一个对应进行复制有二种比较好的方式,一种就是序列化,另一种就是克隆。使用序列化进行复制很方便,因为此种方式会自动进行深层复制,只需要我们将要序列化的对象所对应的类实现序列化标示性接口Serializable,它就会将对象里所引用的其他对象一并复制,但此种效率不及 Object里的clone克隆方法。不过使用clone进行克隆却是浅复制,它不会自动将对象里所引用的其他对象进行深层克隆,所以如果我们想要进行深层复制时,需要覆写Object中的clone方法,对需要进行深层复制的域进行单独处理,所以应用起来比较麻烦,正是因为这样繁琐,下面我采用了反射的方式来进行深层克隆clone,其只需要克隆的类继承该类DeepClone即可。

 

Object 里面的clone方法之所以快, 可能就是因为它是浅克隆, 如果用反射的方法来做深克隆的话, 个人觉得效率应该要比序列化差很多。 那就完全失去了这么做的意义了。 如果楼主能提供些branchmark的数据, 来比较一下序列化和反射两种深度克隆的性能, 会更有说服力。

 

0 请登录后投票
   发表时间:2010-05-06  
做了个简单的测试:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Test {
	private final static ValueBean vb = new ValueBean();


	public static void main(String[] args) throws Exception {
		testClone(1);
		testSerial(1);
		
		testClone(100);
		testSerial(100);
	}

	static void testSerial(int times) throws IOException, ClassNotFoundException {
		ByteArrayOutputStream bos;
		ObjectOutputStream oos;
		ByteArrayInputStream bin;
		ObjectInputStream ois;
		long startTime = System.currentTimeMillis();

		for (int i = 0; i < times; i++) {
			bos = new ByteArrayOutputStream();
			oos = new ObjectOutputStream(bos);
			oos.writeObject(vb);
			bin = new ByteArrayInputStream(bos.toByteArray());
			ois = new ObjectInputStream(bin);
			ois.readObject();
		}

		System.out.println("testSerial:" + (System.currentTimeMillis() - startTime));
	}

	static void testClone(int times) throws CloneNotSupportedException {
		long startTime = System.currentTimeMillis();

		for (int i = 0; i < times; i++) {
			Test.vb.clone();
		}
		System.out.println("testClone:" + (System.currentTimeMillis() - startTime));
	}
}



某次输出结果:
testClone:2
testSerial:45
testClone:20
testSerial:196

好像还是比序列化方式的要快,如果不使用反射我想会更快!
0 请登录后投票
   发表时间:2010-05-07  
利用反射来克隆,怎么解决环依赖的问题?
最常见的是hibernate的父子相互依赖
不瞎折腾还是序列化方便。不过更好的方式可以参考blazeds。
java序列化到as,as序列化到java。都不依赖于Serializable
0 请登录后投票
   发表时间:2010-05-07  
xtlincong 写道
利用反射来克隆,怎么解决环依赖的问题?
最常见的是hibernate的父子相互依赖
不瞎折腾还是序列化方便。不过更好的方式可以参考blazeds。
java序列化到as,as序列化到java。都不依赖于Serializable


这只是一种技术,实现的方式有很多种,这一种当然不一定是最好的,但当你把这些技术弄清后,我想没有你解次不了的问题。这里我只是使用了克隆的方式来实现 ,序列化当然更简单,当然可能也还有其他的办法,就像你说的使用blazeds技术也不是不可以。
我是抱着一种学习的态度去写的这个东西,不一定适合真真的应用中,但这不代表我们不去学这里基本性的东西,一旦掌握了它们,使用其他人写的组件我想也会理解得更好。
0 请登录后投票
   发表时间:2010-05-07  
为什么不用ObjectStream进行深克隆,而要自己写反射????
0 请登录后投票
   发表时间:2010-05-07  
torycatkin 写道
为什么不用ObjectStream进行深克隆,而要自己写反射????

学习学习而已,也说不上为什么?序列化不用怎么写就可以做到,没什么劲。不过你硬要我说出一点就是clone比序列化要快吧,哪怕是在用反射的情况下,如果不用反射我想更快吧
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics